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) {
123 const WANT: [&str; 2] = [".scc/*", "!.scc/intent.yaml"];
124 let gi = root.join(".gitignore");
125 let content = std::fs::read_to_string(&gi).unwrap_or_default();
126 let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
127 let mut changed = false;
128 lines.retain(|l| {
129 let bare = l.trim() == ".scc/" || l.trim() == ".scc";
130 if bare {
131 changed = true;
132 }
133 !bare
134 });
135 for line in WANT {
136 if !lines.iter().any(|l| l.trim() == line) {
137 lines.push(line.to_string());
138 changed = true;
139 }
140 }
141 if changed {
142 let mut out = lines.join("\n");
143 out.push('\n');
144 let _ = std::fs::write(&gi, out);
145 }
146}
147
148pub struct Engine<'a> {
150 pub store: &'a Store,
151 pub graph: RealityGraph,
152 pub settings: scc_context::ContextSettings,
153 pub stale: Vec<String>,
154}
155
156pub use crate::plugins::cache_key_fragment;
158pub fn open_engine<'a>(
160 store: &'a Store,
161 config: &Config,
162 stale: Vec<String>,
163) -> crate::Result<Engine<'a>> {
164 let graph = RealityGraph::load(store)?;
165 let plugin_salt = cache_key_fragment(&crate::plugins::active(&store.root, config));
169 let settings = scc_context::ContextSettings {
170 startup_tokens: config.context.startup_tokens,
171 task_tokens: config.context.task_tokens,
172 atlas_tokens: config.context.atlas_tokens,
173 detail_tokens: config.context.detail_tokens,
174 include_low_confidence_inference: config.context.include_low_confidence_inference,
175 rank_salt: format!(
176 "{}:{}:{}:{}",
177 config.inference.enabled,
178 config.inference.embedding_model,
179 config.inference.rerank_model,
180 plugin_salt,
181 ),
182 pack_allocator: scc_context::PackAllocator::AdaptivePriority,
183 };
184 Ok(Engine {
185 store,
186 graph,
187 settings,
188 stale,
189 })
190}
191
192impl Engine<'_> {
194 pub fn ctx(&self) -> ContextCompiler<'_> {
196 ContextCompiler::new(
197 self.store,
198 &self.graph,
199 self.settings.clone(),
200 self.stale.clone(),
201 )
202 }
203
204 pub fn context(&self) -> SccContext<'_> {
206 SccContext { engine: self }
207 }
208
209 pub fn ranking(&self) -> crate::ranking::Ranker<'_> {
211 crate::ranking::Ranker::new(self)
212 }
213
214 pub fn operations(&self) -> Operations<'_> {
216 Operations { engine: self }
217 }
218
219 pub fn invoke(&self, operation: &str, input: serde_json::Value) -> crate::Result<serde_json::Value> {
221 crate::invoke::invoke(&self.store.root, operation, input)
222 }
223
224 pub fn invoke_session(
226 &self,
227 session: &Session,
228 operation: &str,
229 input: serde_json::Value,
230 ) -> crate::Result<serde_json::Value> {
231 let config = load_config(&self.store.root)?;
232 let live = open_session(self.store, &config)?;
233 if live != *session {
234 let mut drifted = Vec::new();
235 if live.repo_id != session.repo_id { drifted.push("repo_id"); }
236 if live.revision != session.revision { drifted.push("revision"); }
237 if live.epoch != session.epoch { drifted.push("epoch"); }
238 if live.config_hash != session.config_hash { drifted.push("config"); }
239 if live.plugin_lock != session.plugin_lock { drifted.push("plugins"); }
240 if live.rank_salt != session.rank_salt { drifted.push("rank_salt"); }
241 return Err(crate::EngineError::Other(format!(
242 "session stale (drifted: {}); re-open with workspace.session",
243 drifted.join(", ")
244 )));
245 }
246 crate::invoke::invoke(&self.store.root, operation, input)
247 }
248}
249
250pub fn stale_paths(store: &Store) -> crate::Result<Vec<String>> {
252 let config = load_config(&store.root)?;
253 let scanned = scc_indexer::scan::scan_repo(&store.root, &config.index).map_err(scc_indexer::IndexError::from)?;
254 let mut fresh: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
255 for f in &scanned {
256 fresh.insert(f.path.as_str(), f.hash.as_str());
257 }
258 let mut out = Vec::new();
259 let mut indexed = std::collections::HashSet::new();
260 for (path, hash, _lang, _kind, _size) in store.all_files()? {
261 indexed.insert(path.clone());
262 match fresh.get(path.as_str()) {
263 Some(current) if *current == hash.as_str() => {}
264 Some(_) => out.push(path),
265 None => out.push(path),
266 }
267 }
268 for f in scanned {
269 if !indexed.contains(&f.path) {
270 out.push(f.path);
271 }
272 }
273 out.sort();
274 out.dedup();
275 Ok(out)
276}
277
278#[allow(dead_code)]
279pub struct Operations<'a> {
281 engine: &'a Engine<'a>,
282}
283
284impl Operations<'_> {
286 pub fn list(&self) -> &'static [crate::ops::OperationDescriptor] {
288 crate::ops::OPERATIONS
289 }
290
291 pub fn describe(&self, id: &str) -> Option<&'static crate::ops::OperationDescriptor> {
293 crate::ops::describe(id)
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
305pub struct Session {
307 pub repo_id: String,
308 pub revision: String,
309 pub epoch: String,
310 pub config_hash: String,
311 pub plugin_lock: Vec<serde_json::Value>,
312 pub rank_salt: String,
313}
314
315pub fn open_session(store: &Store, config: &Config) -> crate::Result<Session> {
317 let repo = store.repository();
318 let head = store.revisions()?.into_iter().last().map(|r| r.rev).unwrap_or(0);
319 let epoch = store.model_epoch()?.composite(&head.to_string());
320 let revision = store
321 .snapshot_status()?
322 .map(|(s, _)| s.revision)
323 .unwrap_or_else(|| "not-indexed".to_string());
324 let engine = open_engine(store, config, stale_paths(store)?)?;
325 Ok(Session {
326 repo_id: repo.id,
327 revision,
328 epoch,
329 config_hash: scc_indexer::semantic_config_hash(config),
330 plugin_lock: crate::plugins::lock_entries(&crate::plugins::active(&store.root, config)),
331 rank_salt: engine.settings.rank_salt,
332 })
333}
334
335pub fn session_is_current(store: &Store, config: &Config, session: &Session) -> crate::Result<bool> {
337 let live = open_session(store, config)?;
338 Ok(live == *session)
339}
340
341