Skip to main content

scc_engine/
index.rs

1//! Engine index operations: full index, path refresh, resolution.
2//!
3//! Moved verbatim from `scc-cli` lib.rs / commands.rs: the revision-after-
4//! recompile ordering contract and the no-change fast path move WITH the
5//! code (parity tests enforce byte-identical reports).
6
7use std::path::Path;
8
9// trace:exempt reason=internal-detail
10pub fn full(root: &Path, config: &scc_indexer::Config) -> crate::Result<scc_indexer::IndexReport> {
11    crate::workspace::ensure_scc_ignored(root);
12    crate::workspace::resilient_index(root, || {
13        let (store, quarantined) = crate::workspace::open_store_recovering(root)?;
14        crate::workspace::report_quarantine(&quarantined);
15        let indexer = scc_indexer::Indexer::new(store, config.clone());
16        let report = indexer.index()?;
17        let store = crate::workspace::open_store(root)?;
18        if config.index.auto_resolve {
19            let _ = scc_indexer::resolver::resolve_repository(
20                &store,
21                root,
22                scc_indexer::resolver::MAX_CALL_SITES,
23            );
24        }
25        let unchanged = report.changed == 0 && report.removed == 0 && !config.index.auto_resolve;
26        let extractor_current = store
27            .revisions()
28            .map(|rs| {
29                rs.into_iter().last().map(|h| {
30                    h.extractor_version
31                        == format!(
32                            "store:{};core:{}",
33                            scc_store::SCHEMA_VERSION,
34                            scc_core::SCHEMA_VERSION
35                        )
36                })
37                .unwrap_or(false)
38            })
39            .unwrap_or(false);
40        if !(unchanged && extractor_current) {
41            recompile_with_signals(root, config, &store)?;
42        }
43        let _ = store.record_current_revision_with_config(
44            &scc_indexer::semantic_config_hash(config),
45        )?;
46        let _ = store.prune_revisions(config.history.max_revisions);
47        Ok(report)
48    })
49}
50
51// trace:exempt reason=internal-detail
52pub fn refresh_paths(root: &Path, config: &scc_indexer::Config, paths: &[String]) -> crate::Result<scc_indexer::IndexReport> {
53    let (store, quarantined) = crate::workspace::open_store_recovering(root)?;
54    crate::workspace::report_quarantine(&quarantined);
55    let indexer = scc_indexer::Indexer::new(crate::workspace::open_store(root)?, config.clone());
56    let report = indexer.refresh_paths(paths)?;
57    drop(indexer);
58    recompile_with_signals(root, config, &store)?;
59    let _ = store.record_current_revision_with_config(
60        &scc_indexer::semantic_config_hash(config),
61    )?;
62    let _ = store.prune_revisions(config.history.max_revisions);
63    Ok(report)
64}
65
66// trace:exempt reason=internal-detail
67pub fn recompile(store: &scc_store::Store) -> crate::Result<scc_graph::RecompileReport> {
68    Ok(scc_graph::recompile(store)?)
69}
70
71/// Recompile with plugin component signals (§31): collects
72/// `components.signals` from active plugins, then runs the pipeline with
73/// them. Signal failures degrade to empty (the collector never errors).
74// trace:exempt reason=internal-detail
75pub fn recompile_with_signals(
76    root: &std::path::Path,
77    config: &scc_indexer::Config,
78    store: &scc_store::Store,
79) -> crate::Result<scc_graph::RecompileReport> {
80    let signals = component_signals(root, config);
81    Ok(scc_graph::CompilationPipeline::new(store)
82        .component_signals(signals)
83        .run()?)
84}
85
86/// Collect component signals (§31 ComponentSignalProvider) from plugins
87/// answering the `components.signals` operation with
88/// `{"signals": [{name, dirs, provider?}]}`. Unknown/dead plugins, bad
89/// shapes, and empty names/dirs degrade to abstention (never fail the
90/// recompile — a contributor that cannot name a component abstains).
91// trace:exempt reason=internal-detail
92pub fn component_signals(
93    root: &std::path::Path,
94    config: &scc_indexer::Config,
95) -> Vec<scc_graph::components::ComponentSignal> {
96    let mut ap = crate::plugins::active(root, config);
97    let mut out = Vec::new();
98    for plug in ap.plugins.clone() {
99        let pid = plug.manifest.id.clone();
100        let provides = plug.manifest.operations.iter().any(|o| o == "components.signals")
101            || plug.manifest.extensions.iter().any(|e| e.extension_type == "component-signal");
102        if !provides {
103            continue;
104        }
105        match scc_plugin_host::call(&plug, "components.signals", serde_json::json!({}), None) {
106            Ok(v) => {
107                if let Some(arr) = v.get("signals").and_then(|x| x.as_array()) {
108                    for e in arr {
109                        let name = e.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string();
110                        let dirs: Vec<String> = e.get("dirs").and_then(|x| x.as_array()).map(|a| {
111                            a.iter().filter_map(|d| d.as_str().map(str::to_string)).collect()
112                        }).unwrap_or_default();
113                        if name.is_empty() || dirs.is_empty() {
114                            continue;
115                        }
116                        out.push(scc_graph::components::ComponentSignal {
117                            name,
118                            dirs,
119                            provider: e.get("provider").and_then(|x| x.as_str()).unwrap_or(&pid).to_string(),
120                        });
121                    }
122                }
123            }
124            Err(e) => {
125                ap.diagnostics.push(scc_plugin_host::PluginDiagnostic {
126                    plugin: pid, operation: "components.signals".into(),
127                    error: e.to_string(), action: "skipped".into(),
128                });
129            }
130        }
131    }
132    out
133}
134
135// trace:exempt reason=internal-detail
136pub fn resolve_and_recompile(root: &Path) -> crate::Result<scc_indexer::resolver::ResolveReport> {
137    let store = crate::workspace::open_store(root)?;
138    let report = scc_indexer::resolver::resolve_repository(
139        &store,
140        root,
141        scc_indexer::resolver::MAX_CALL_SITES,
142    )
143    .map_err(|e| crate::EngineError::Other(e.to_string()))?;
144    let config = crate::workspace::load_config(root)?;
145    recompile_with_signals(root, &config, &store)?;
146    Ok(report)
147}