1use std::path::Path;
8
9pub 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
51pub 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 if !report.mutated {
64 return Ok(report);
65 }
66 let signals = component_signals(root, config);
71 let fresh = crate::workspace::open_store(root)?;
72 let (_scoped, closure) = scc_graph::recompile_scoped_with_owners(
73 &fresh,
74 &report.affected_files,
75 &report.affected_components,
76 signals,
77 )?;
78 eprintln!(
79 "[scc] refresh closure: {} file(s) -> {} component(s), {} flow(s){}",
80 report.affected_files.len(),
81 closure.components.len(),
82 closure.flows.len(),
83 if closure.complete { " (UNBOUNDED: full pipeline required)" } else { "" },
84 );
85 recompile_with_signals(root, config, &store)?;
86 let _ = store.record_current_revision_with_config(
87 &scc_indexer::semantic_config_hash(config),
88 )?;
89 let _ = store.prune_revisions(config.history.max_revisions);
90 Ok(report)
91}
92
93pub fn recompile(store: &scc_store::Store) -> crate::Result<scc_graph::RecompileReport> {
95 Ok(scc_graph::recompile(store)?)
96}
97
98pub fn recompile_with_signals(
103 root: &std::path::Path,
104 config: &scc_indexer::Config,
105 store: &scc_store::Store,
106) -> crate::Result<scc_graph::RecompileReport> {
107 let signals = component_signals(root, config);
108 Ok(scc_graph::CompilationPipeline::new(store)
109 .component_signals(signals)
110 .run()?)
111}
112
113pub fn component_signals(
120 root: &std::path::Path,
121 config: &scc_indexer::Config,
122) -> Vec<scc_graph::components::ComponentSignal> {
123 let mut ap = crate::plugins::active(root, config);
124 let mut out = Vec::new();
125 for plug in ap.plugins.clone() {
126 let pid = plug.manifest.id.clone();
127 let provides = plug.manifest.operations.iter().any(|o| o == "components.signals")
128 || plug.manifest.extensions.iter().any(|e| e.extension_type == "component-signal");
129 if !provides {
130 continue;
131 }
132 match scc_plugin_host::call(&plug, "components.signals", serde_json::json!({}), None) {
133 Ok(v) => {
134 if let Some(arr) = v.get("signals").and_then(|x| x.as_array()) {
135 for e in arr {
136 let name = e.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string();
137 let dirs: Vec<String> = e.get("dirs").and_then(|x| x.as_array()).map(|a| {
138 a.iter().filter_map(|d| d.as_str().map(str::to_string)).collect()
139 }).unwrap_or_default();
140 if name.is_empty() || dirs.is_empty() {
141 continue;
142 }
143 out.push(scc_graph::components::ComponentSignal {
144 name,
145 dirs,
146 provider: e.get("provider").and_then(|x| x.as_str()).unwrap_or(&pid).to_string(),
147 });
148 }
149 }
150 }
151 Err(e) => {
152 ap.diagnostics.push(scc_plugin_host::PluginDiagnostic {
153 plugin: pid, operation: "components.signals".into(),
154 error: e.to_string(), action: "skipped".into(),
155 });
156 }
157 }
158 }
159 out
160}
161
162pub fn resolve_and_recompile(root: &Path) -> crate::Result<scc_indexer::resolver::ResolveReport> {
164 let store = crate::workspace::open_store(root)?;
165 let report = scc_indexer::resolver::resolve_repository(
166 &store,
167 root,
168 scc_indexer::resolver::MAX_CALL_SITES,
169 )
170 .map_err(|e| crate::EngineError::Other(e.to_string()))?;
171 let config = crate::workspace::load_config(root)?;
172 recompile_with_signals(root, &config, &store)?;
173 Ok(report)
174}