1use crate::context::SccContext;
6use scc_context::ContextCompiler;
7use scc_graph::RealityGraph;
8use scc_indexer::Config;
9use scc_store::Store;
10use std::path::{Path, PathBuf};
11
12pub const SCC_DIR: &str = ".scc";
14pub const DB_FILE: &str = "scc.db";
16pub const CONFIG_FILE: &str = "config.yaml";
18pub const CHECKPOINT_FILE: &str = "checkpoint.json";
20
21pub fn state_dir(root: &Path) -> PathBuf {
23 match std::env::var("SCC_STATE_DIR") {
24 Ok(dir) if !dir.is_empty() => PathBuf::from(dir),
25 _ => scc_dir(root),
26 }
27}
28
29pub fn scc_dir(root: &Path) -> PathBuf {
31 root.join(SCC_DIR)
32}
33
34pub fn db_path(root: &Path) -> PathBuf {
36 state_dir(root).join(DB_FILE)
37}
38
39pub fn config_path(root: &Path) -> PathBuf {
41 scc_dir(root).join(CONFIG_FILE)
42}
43
44pub fn checkpoint_path(root: &Path) -> PathBuf {
46 state_dir(root).join(CHECKPOINT_FILE)
47}
48
49pub fn find_root(start: &Path) -> PathBuf {
51 let mut dir = Some(start.to_path_buf());
52 while let Some(d) = dir {
53 if d.join(".git").exists() || d.join(SCC_DIR).exists() {
54 return d;
55 }
56 dir = d.parent().map(|p| p.to_path_buf());
57 }
58 start.to_path_buf()
59}
60
61pub fn load_config(root: &Path) -> crate::Result<Config> {
63 let p = config_path(root);
64 if p.exists() {
65 Ok(Config::load(&p)?)
66 } else {
67 Ok(Config::default())
68 }
69}
70
71pub fn open_store(root: &Path) -> crate::Result<Store> {
73 let dir = state_dir(root);
74 std::fs::create_dir_all(&dir)?;
75 Ok(Store::open(&db_path(root), root)?)
76}
77
78pub fn open_store_recovering(root: &Path) -> crate::Result<(Store, Option<PathBuf>)> {
80 let dir = state_dir(root);
81 std::fs::create_dir_all(&dir)?;
82 Ok(Store::open_recovering(&db_path(root), root)?)
83}
84
85pub fn report_quarantine(quarantined: &Option<PathBuf>) {
87 if let Some(q) = quarantined {
88 eprintln!(
89 "warning: existing index was corrupt (malformed database); quarantined to {} and rebuilding from scratch.",
90 q.display()
91 );
92 }
93}
94
95pub fn is_store_corruption(e: &crate::EngineError) -> bool {
97 match e {
98 crate::EngineError::Store(s) => Store::is_corruption(s),
99 crate::EngineError::Index(scc_indexer::IndexError::Store(s)) => Store::is_corruption(s),
100 crate::EngineError::Graph(scc_graph::GraphError::Store(s)) => Store::is_corruption(s),
101 _ => false,
102 }
103}
104
105pub fn resilient_index<T>(
107 root: &Path,
108 mut attempt: impl FnMut() -> crate::Result<T>,
109) -> crate::Result<T> {
110 match attempt() {
111 Ok(v) => Ok(v),
112 Err(e) if is_store_corruption(&e) => {
113 let q = Store::quarantine_db(&db_path(root))?;
114 report_quarantine(&Some(q));
115 attempt()
116 }
117 Err(e) => Err(e),
118 }
119}
120
121pub fn ensure_scc_ignored(root: &Path) {
127 const WANT: [&str; 4] = [".scc/*", "!.scc/intent.yaml", "!.scc/plugins.toml", "!.scc/plugins.lock"];
128 let gi = root.join(".gitignore");
129 let content = std::fs::read_to_string(&gi).unwrap_or_default();
130 let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
131 let mut changed = false;
132 lines.retain(|l| {
133 let bare = l.trim() == ".scc/" || l.trim() == ".scc";
134 if bare {
135 changed = true;
136 }
137 !bare
138 });
139 for line in WANT {
140 if !lines.iter().any(|l| l.trim() == line) {
141 lines.push(line.to_string());
142 changed = true;
143 }
144 }
145 if changed {
146 let mut out = lines.join("\n");
147 out.push('\n');
148 let _ = std::fs::write(&gi, out);
149 }
150}
151
152pub struct Engine<'a> {
154 pub store: &'a Store,
155 pub graph: RealityGraph,
156 pub settings: scc_context::ContextSettings,
157 pub stale: Vec<String>,
158}
159
160pub use crate::plugins::cache_key_fragment;
162pub fn open_engine<'a>(
164 store: &'a Store,
165 config: &Config,
166 stale: Vec<String>,
167) -> crate::Result<Engine<'a>> {
168 let graph = RealityGraph::load(store)?;
169 let plugin_salt = cache_key_fragment(&crate::plugins::active(&store.root, config));
173 let settings = scc_context::ContextSettings {
174 startup_tokens: config.context.startup_tokens,
175 task_tokens: config.context.task_tokens,
176 atlas_tokens: config.context.atlas_tokens,
177 detail_tokens: config.context.detail_tokens,
178 include_low_confidence_inference: config.context.include_low_confidence_inference,
179 rank_salt: format!(
180 "{}:{}:{}:{}",
181 config.inference.enabled,
182 config.inference.embedding_model,
183 config.inference.rerank_model,
184 plugin_salt,
185 ),
186 pack_allocator: scc_context::PackAllocator::AdaptivePriority,
187 };
188 Ok(Engine {
189 store,
190 graph,
191 settings,
192 stale,
193 })
194}
195
196impl Engine<'_> {
198 pub fn ctx(&self) -> ContextCompiler<'_> {
200 ContextCompiler::new(
201 self.store,
202 &self.graph,
203 self.settings.clone(),
204 self.stale.clone(),
205 )
206 }
207
208 pub fn context(&self) -> SccContext<'_> {
210 SccContext { engine: self }
211 }
212
213 pub fn ranking(&self) -> crate::ranking::Ranker<'_> {
215 crate::ranking::Ranker::new(self)
216 }
217
218 pub fn operations(&self) -> Operations<'_> {
220 Operations { engine: self }
221 }
222
223 pub fn invoke(&self, operation: &str, input: serde_json::Value) -> crate::Result<serde_json::Value> {
225 crate::invoke::invoke(&self.store.root, operation, input)
226 }
227
228 pub fn invoke_session(
230 &self,
231 session: &Session,
232 operation: &str,
233 input: serde_json::Value,
234 ) -> crate::Result<serde_json::Value> {
235 let config = load_config(&self.store.root)?;
236 let live = open_session(self.store, &config)?;
237 if live != *session {
238 let mut drifted = Vec::new();
239 if live.repo_id != session.repo_id { drifted.push("repo_id"); }
240 if live.revision != session.revision { drifted.push("revision"); }
241 if live.epoch != session.epoch { drifted.push("epoch"); }
242 if live.config_hash != session.config_hash { drifted.push("config"); }
243 if live.plugin_lock != session.plugin_lock { drifted.push("plugins"); }
244 if live.rank_salt != session.rank_salt { drifted.push("rank_salt"); }
245 return Err(crate::EngineError::Other(format!(
246 "session stale (drifted: {}); re-open with workspace.session",
247 drifted.join(", ")
248 )));
249 }
250 crate::invoke::invoke(&self.store.root, operation, input)
251 }
252}
253
254pub fn stale_paths(store: &Store) -> crate::Result<Vec<String>> {
256 let config = load_config(&store.root)?;
257 let scanned = scc_indexer::scan::scan_repo(&store.root, &config.index).map_err(scc_indexer::IndexError::from)?;
258 let mut fresh: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
259 for f in &scanned {
260 fresh.insert(f.path.as_str(), f.hash.as_str());
261 }
262 let mut out = Vec::new();
263 let mut indexed = std::collections::HashSet::new();
264 for (path, hash, _lang, _kind, _size) in store.all_files()? {
265 indexed.insert(path.clone());
266 match fresh.get(path.as_str()) {
267 Some(current) if *current == hash.as_str() => {}
268 Some(_) => out.push(path),
269 None => out.push(path),
270 }
271 }
272 for f in scanned {
273 if !indexed.contains(&f.path) {
274 out.push(f.path);
275 }
276 }
277 out.sort();
278 out.dedup();
279 Ok(out)
280}
281
282#[allow(dead_code)]
283pub struct Operations<'a> {
285 engine: &'a Engine<'a>,
286}
287
288impl Operations<'_> {
290 pub fn list(&self) -> &'static [crate::ops::OperationDescriptor] {
292 crate::ops::OPERATIONS
293 }
294
295 pub fn describe(&self, id: &str) -> Option<&'static crate::ops::OperationDescriptor> {
297 crate::ops::describe(id)
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
309pub struct Session {
311 pub repo_id: String,
312 pub revision: String,
313 pub epoch: String,
314 pub config_hash: String,
315 pub plugin_lock: Vec<serde_json::Value>,
316 pub rank_salt: String,
317 #[serde(default)]
321 pub pipeline: String,
322}
323
324pub fn open_session(store: &Store, config: &Config) -> crate::Result<Session> {
326 let repo = store.repository();
327 let head = store.revisions()?.into_iter().last().map(|r| r.rev).unwrap_or(0);
328 let epoch = store.model_epoch()?.composite(&head.to_string());
329 let revision = store
330 .snapshot_status()?
331 .map(|(s, _)| s.revision)
332 .unwrap_or_else(|| "not-indexed".to_string());
333 let engine = open_engine(store, config, stale_paths(store)?)?;
334 let ap = crate::plugins::active(&store.root, config);
335 Ok(Session {
336 repo_id: repo.id,
337 revision,
338 epoch,
339 config_hash: scc_indexer::semantic_config_hash(config),
340 plugin_lock: crate::plugins::lock_entries(&ap),
341 rank_salt: engine.settings.rank_salt.clone(),
342 pipeline: crate::plugins::cache_key_fragment(&ap),
343 })
344}
345
346pub fn session_is_current(store: &Store, config: &Config, session: &Session) -> crate::Result<bool> {
348 let live = open_session(store, config)?;
349 Ok(live == *session)
350}
351
352