use crate::context::SccContext;
use scc_context::ContextCompiler;
use scc_graph::RealityGraph;
use scc_indexer::Config;
use scc_store::Store;
use std::path::{Path, PathBuf};
pub const SCC_DIR: &str = ".scc";
pub const DB_FILE: &str = "scc.db";
pub const CONFIG_FILE: &str = "config.yaml";
pub const CHECKPOINT_FILE: &str = "checkpoint.json";
pub fn state_dir(root: &Path) -> PathBuf {
match std::env::var("SCC_STATE_DIR") {
Ok(dir) if !dir.is_empty() => PathBuf::from(dir),
_ => scc_dir(root),
}
}
pub fn scc_dir(root: &Path) -> PathBuf {
root.join(SCC_DIR)
}
pub fn db_path(root: &Path) -> PathBuf {
state_dir(root).join(DB_FILE)
}
pub fn config_path(root: &Path) -> PathBuf {
scc_dir(root).join(CONFIG_FILE)
}
pub fn checkpoint_path(root: &Path) -> PathBuf {
state_dir(root).join(CHECKPOINT_FILE)
}
pub fn find_root(start: &Path) -> PathBuf {
let mut dir = Some(start.to_path_buf());
while let Some(d) = dir {
if d.join(".git").exists() || d.join(SCC_DIR).exists() {
return d;
}
dir = d.parent().map(|p| p.to_path_buf());
}
start.to_path_buf()
}
pub fn load_config(root: &Path) -> crate::Result<Config> {
let p = config_path(root);
if p.exists() {
Ok(Config::load(&p)?)
} else {
Ok(Config::default())
}
}
pub fn open_store(root: &Path) -> crate::Result<Store> {
let dir = state_dir(root);
std::fs::create_dir_all(&dir)?;
Ok(Store::open(&db_path(root), root)?)
}
pub fn open_store_recovering(root: &Path) -> crate::Result<(Store, Option<PathBuf>)> {
let dir = state_dir(root);
std::fs::create_dir_all(&dir)?;
Ok(Store::open_recovering(&db_path(root), root)?)
}
pub fn report_quarantine(quarantined: &Option<PathBuf>) {
if let Some(q) = quarantined {
eprintln!(
"warning: existing index was corrupt (malformed database); quarantined to {} and rebuilding from scratch.",
q.display()
);
}
}
pub fn is_store_corruption(e: &crate::EngineError) -> bool {
match e {
crate::EngineError::Store(s) => Store::is_corruption(s),
crate::EngineError::Index(scc_indexer::IndexError::Store(s)) => Store::is_corruption(s),
crate::EngineError::Graph(scc_graph::GraphError::Store(s)) => Store::is_corruption(s),
_ => false,
}
}
pub fn resilient_index<T>(
root: &Path,
mut attempt: impl FnMut() -> crate::Result<T>,
) -> crate::Result<T> {
match attempt() {
Ok(v) => Ok(v),
Err(e) if is_store_corruption(&e) => {
let q = Store::quarantine_db(&db_path(root))?;
report_quarantine(&Some(q));
attempt()
}
Err(e) => Err(e),
}
}
pub fn ensure_scc_ignored(root: &Path) {
const WANT: [&str; 4] = [".scc/*", "!.scc/intent.yaml", "!.scc/plugins.toml", "!.scc/plugins.lock"];
let gi = root.join(".gitignore");
let content = std::fs::read_to_string(&gi).unwrap_or_default();
let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
let mut changed = false;
lines.retain(|l| {
let bare = l.trim() == ".scc/" || l.trim() == ".scc";
if bare {
changed = true;
}
!bare
});
for line in WANT {
if !lines.iter().any(|l| l.trim() == line) {
lines.push(line.to_string());
changed = true;
}
}
if changed {
let mut out = lines.join("\n");
out.push('\n');
let _ = std::fs::write(&gi, out);
}
}
pub struct Engine<'a> {
pub store: &'a Store,
pub graph: RealityGraph,
pub settings: scc_context::ContextSettings,
pub stale: Vec<String>,
}
pub use crate::plugins::cache_key_fragment;
pub fn open_engine<'a>(
store: &'a Store,
config: &Config,
stale: Vec<String>,
) -> crate::Result<Engine<'a>> {
let graph = RealityGraph::load(store)?;
let plugin_salt = cache_key_fragment(&crate::plugins::active(&store.root, config));
let settings = scc_context::ContextSettings {
startup_tokens: config.context.startup_tokens,
task_tokens: config.context.task_tokens,
atlas_tokens: config.context.atlas_tokens,
detail_tokens: config.context.detail_tokens,
include_low_confidence_inference: config.context.include_low_confidence_inference,
rank_salt: format!(
"{}:{}:{}:{}",
config.inference.enabled,
config.inference.embedding_model,
config.inference.rerank_model,
plugin_salt,
),
pack_allocator: scc_context::PackAllocator::AdaptivePriority,
};
Ok(Engine {
store,
graph,
settings,
stale,
})
}
impl Engine<'_> {
pub fn ctx(&self) -> ContextCompiler<'_> {
ContextCompiler::new(
self.store,
&self.graph,
self.settings.clone(),
self.stale.clone(),
)
}
pub fn context(&self) -> SccContext<'_> {
SccContext { engine: self }
}
pub fn ranking(&self) -> crate::ranking::Ranker<'_> {
crate::ranking::Ranker::new(self)
}
pub fn operations(&self) -> Operations<'_> {
Operations { engine: self }
}
pub fn invoke(&self, operation: &str, input: serde_json::Value) -> crate::Result<serde_json::Value> {
crate::invoke::invoke(&self.store.root, operation, input)
}
pub fn invoke_session(
&self,
session: &Session,
operation: &str,
input: serde_json::Value,
) -> crate::Result<serde_json::Value> {
let config = load_config(&self.store.root)?;
let live = open_session(self.store, &config)?;
if live != *session {
let mut drifted = Vec::new();
if live.repo_id != session.repo_id { drifted.push("repo_id"); }
if live.revision != session.revision { drifted.push("revision"); }
if live.epoch != session.epoch { drifted.push("epoch"); }
if live.config_hash != session.config_hash { drifted.push("config"); }
if live.plugin_lock != session.plugin_lock { drifted.push("plugins"); }
if live.rank_salt != session.rank_salt { drifted.push("rank_salt"); }
return Err(crate::EngineError::Other(format!(
"session stale (drifted: {}); re-open with workspace.session",
drifted.join(", ")
)));
}
crate::invoke::invoke(&self.store.root, operation, input)
}
}
pub fn stale_paths(store: &Store) -> crate::Result<Vec<String>> {
let config = load_config(&store.root)?;
let scanned = scc_indexer::scan::scan_repo(&store.root, &config.index).map_err(scc_indexer::IndexError::from)?;
let mut fresh: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
for f in &scanned {
fresh.insert(f.path.as_str(), f.hash.as_str());
}
let mut out = Vec::new();
let mut indexed = std::collections::HashSet::new();
for (path, hash, _lang, _kind, _size) in store.all_files()? {
indexed.insert(path.clone());
match fresh.get(path.as_str()) {
Some(current) if *current == hash.as_str() => {}
Some(_) => out.push(path),
None => out.push(path),
}
}
for f in scanned {
if !indexed.contains(&f.path) {
out.push(f.path);
}
}
out.sort();
out.dedup();
Ok(out)
}
#[allow(dead_code)]
pub struct Operations<'a> {
engine: &'a Engine<'a>,
}
impl Operations<'_> {
pub fn list(&self) -> &'static [crate::ops::OperationDescriptor] {
crate::ops::OPERATIONS
}
pub fn describe(&self, id: &str) -> Option<&'static crate::ops::OperationDescriptor> {
crate::ops::describe(id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Session {
pub repo_id: String,
pub revision: String,
pub epoch: String,
pub config_hash: String,
pub plugin_lock: Vec<serde_json::Value>,
pub rank_salt: String,
#[serde(default)]
pub pipeline: String,
}
pub fn open_session(store: &Store, config: &Config) -> crate::Result<Session> {
let repo = store.repository();
let head = store.revisions()?.into_iter().last().map(|r| r.rev).unwrap_or(0);
let epoch = store.model_epoch()?.composite(&head.to_string());
let revision = store
.snapshot_status()?
.map(|(s, _)| s.revision)
.unwrap_or_else(|| "not-indexed".to_string());
let engine = open_engine(store, config, stale_paths(store)?)?;
let ap = crate::plugins::active(&store.root, config);
Ok(Session {
repo_id: repo.id,
revision,
epoch,
config_hash: scc_indexer::semantic_config_hash(config),
plugin_lock: crate::plugins::lock_entries(&ap),
rank_salt: engine.settings.rank_salt.clone(),
pipeline: crate::plugins::cache_key_fragment(&ap),
})
}
pub fn session_is_current(store: &Store, config: &Config, session: &Session) -> crate::Result<bool> {
let live = open_session(store, config)?;
Ok(live == *session)
}