use std::io::Write;
use std::path::{Path, PathBuf};
pub fn lessons_add(root: &Path, text: &str) -> crate::Result<(String, PathBuf)> {
let dir = crate::workspace::scc_dir(root);
std::fs::create_dir_all(&dir)?;
let path = dir.join("lessons.jsonl");
let n = std::fs::read_to_string(&path)
.map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
.unwrap_or(0);
let id = format!("lesson-{}", n + 1);
let record = serde_json::json!({
"id": id,
"text": text,
"created_at": scc_core::now_rfc3339(),
});
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| crate::EngineError::Other(format!("lessons: {e}")))?;
writeln!(f, "{record}").map_err(|e| crate::EngineError::Other(format!("lessons: {e}")))?;
Ok((id, path))
}
pub fn lessons_list(root: &Path, limit: usize) -> crate::Result<Vec<(String, Vec<String>)>> {
let store = crate::workspace::open_store(root)?;
Ok(scc_indexer::adapters::hindsight::lessons(&store, limit))
}
pub fn beads(root: &Path, limit: usize) -> crate::Result<Vec<String>> {
Ok(scc_indexer::adapters::beads::active_beads(root, limit))
}
pub fn import_evidence(root: &Path, format: &str, file: &str) -> crate::Result<scc_indexer::adapters::ImportReport> {
let store = crate::workspace::open_store(root)?;
let report = match format {
"scip" => scc_indexer::adapters::import_scip(&store, std::path::Path::new(file)),
"ccg" => scc_indexer::adapters::import_ccg(&store, std::path::Path::new(file)),
"gitnexus" => scc_indexer::adapters::gitnexus::import_gitnexus(&store, std::path::Path::new(file))
.map(|r| scc_indexer::adapters::ImportReport {
symbols: r.symbols,
calls: r.edges,
imports: 0,
errors: r.errors,
}),
"beads" => scc_indexer::adapters::beads::import_beads(&store, std::path::Path::new(file))
.map(|r| scc_indexer::adapters::ImportReport {
symbols: r.tasks,
calls: r.dependencies,
imports: r.active,
errors: r.errors,
}),
"cbm" => scc_indexer::adapters::cbm::import_cbm(&store, std::path::Path::new(file))
.map(|r| scc_indexer::adapters::ImportReport {
symbols: r.symbols,
calls: r.relationships,
imports: 0,
errors: r.errors,
}),
"hindsight" => scc_indexer::adapters::hindsight::import_hindsight(&store, std::path::Path::new(file))
.map(|r| scc_indexer::adapters::ImportReport {
symbols: r.lessons,
calls: 0,
imports: 0,
errors: r.errors,
}),
"tracelayer" => scc_indexer::adapters::tracelayer::import_tracelayer(&store, std::path::Path::new(file))
.map(|r| scc_indexer::adapters::ImportReport {
symbols: r.requirements + r.implementations + r.tests + r.decisions,
calls: r.relationships,
imports: r.work_items,
errors: r.errors,
}),
other => {
return import_plugin_evidence(root, other, file);
}
}
.map_err(crate::EngineError::Other)?;
store.bump_epoch(scc_store::ModelEpochKind::Evidence)?;
crate::index::recompile(&store)?;
Ok(report)
}
pub fn runtime_edges(root: &Path) -> crate::Result<Vec<scc_indexer::runtime::RuntimeEdge>> {
let store = crate::workspace::open_store(root)?;
scc_indexer::runtime::runtime_edges(&store).map_err(crate::EngineError::Other)
}
pub fn ingest_runtime(root: &Path, body: &str) -> crate::Result<()> {
let store = crate::workspace::open_store(root)?;
if body.contains("resourceSpans") {
scc_indexer::runtime::ingest_otlp_json(&store, body)
.map_err(crate::EngineError::Other)?;
} else {
scc_indexer::runtime::ingest_simple_edges(&store, body)
.map_err(crate::EngineError::Other)?;
}
ingest_plugin_runtime_edges(root, &store, body);
Ok(())
}
pub fn ingest_plugin_runtime_edges(
root: &Path,
store: &scc_store::Store,
body: &str,
) {
let config = match crate::workspace::load_config(root) {
Ok(c) => c,
Err(_) => return,
};
let ap = crate::plugins::active(root, &config);
for plug in &ap.plugins {
let is_provider = plug.manifest.extensions.iter().any(|e| e.extension_type == "runtime-evidence")
|| plug.manifest.operations.iter().any(|o| o == "runtime.evidence");
if !is_provider {
continue;
}
let out = match scc_plugin_host::call(
plug,
"runtime.evidence",
serde_json::json!({"body": body}),
None,
) {
Ok(v) => v,
Err(_) => continue,
};
let edges = out.get("edges").and_then(|v| v.as_array()).cloned().unwrap_or_default();
if edges.is_empty() {
continue;
}
let payload = serde_json::Value::Array(edges);
let text = payload.to_string();
let _ = scc_indexer::runtime::ingest_simple_edges(store, &text);
}
}
pub fn reconcile(root: &Path) -> crate::Result<scc_indexer::runtime::Reconciliation> {
let store = crate::workspace::open_store(root)?;
scc_indexer::runtime::reconcile(&store).map_err(crate::EngineError::Other)
}
pub fn embeddings_build(root: &Path) -> crate::Result<serde_json::Value> {
use scc_indexer::embed::EmbedConfig;
let store = crate::workspace::open_store(root)?;
let config = crate::workspace::load_config(root)?;
if !config.inference.enabled {
return Err(crate::EngineError::Other(
"inference is disabled — set `inference.enabled: true` in .scc/config.yaml".into(),
));
}
let cfg = EmbedConfig::from_config(&config.inference);
if cfg.is_remote() && !config.security.allow_remote_models {
return Err(crate::EngineError::Other(
"remote inference blocked: repository-derived content would leave the machine — set `security.allow_remote_models: true` to allow it (or use a loopback provider)".into(),
));
}
if store.snapshot_status()?.is_none() {
return Err(crate::EngineError::Other("not indexed — run `scc index` first".into()));
}
let n = scc_indexer::embed::embed_repository(&store, &cfg).map_err(crate::EngineError::Other)?;
store.cache_clear()?;
Ok(serde_json::json!({"stored": n, "model": cfg.model}))
}
pub fn embeddings_get(store: &scc_store::Store, entity_id: &str) -> crate::Result<serde_json::Value> {
match store.get_embedding(entity_id)? {
Some((v, model)) => Ok(serde_json::json!({"id": entity_id, "model": model, "dims": v.len(), "vector": v})),
None => Ok(serde_json::Value::Null),
}
}
pub fn embeddings_status(store: &scc_store::Store) -> crate::Result<serde_json::Value> {
let count = store.embedding_count()?;
Ok(serde_json::json!({"embeddings": count}))
}
pub fn external_docs(root: &Path, dependency: &str) -> crate::Result<String> {
let config = crate::workspace::load_config(root)?;
if config.integrations.context7_command.is_empty() {
return Err(crate::EngineError::Other(
"Context7 is not configured — set integrations.context7_command in .scc/config.yaml".into(),
));
}
let mut client = scc_indexer::adapters::context7::start(&config.integrations.context7_command, root)
.map_err(crate::EngineError::Other)?;
client.docs_for(dependency).map_err(crate::EngineError::Other)
}
pub fn plugin_state_get(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
key: &str,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, false)?;
Ok(store.plugin_state_get(plugin_id, key)?.into())
}
pub fn plugin_state_put(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
key: &str,
value: &str,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, true)?;
store.plugin_state_put(plugin_id, key, value)?;
Ok(serde_json::json!({"ok": true}))
}
pub fn plugin_state_delete(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
key: &str,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, true)?;
store.plugin_state_delete(plugin_id, key)?;
Ok(serde_json::json!({"ok": true}))
}
pub fn plugin_state_scan(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
prefix: &str,
limit: usize,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, false)?;
let rows = store.plugin_state_scan(plugin_id, prefix, limit)?;
Ok(serde_json::json!({
"keys": rows.iter().map(|(k, v)| serde_json::json!({"key": k, "value": v})).collect::<Vec<_>>(),
}))
}
pub fn sidecar_put(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
graph: &str,
key: &str,
value: &str,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, true)?;
store.sidecar_put(plugin_id, graph, key, value)?;
Ok(serde_json::json!({"ok": true}))
}
pub fn sidecar_get(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
graph: &str,
key: &str,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, false)?;
Ok(store.sidecar_get(plugin_id, graph, key)?.into())
}
pub fn sidecar_scan(
store: &scc_store::Store,
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
graph: &str,
prefix: &str,
limit: usize,
) -> crate::Result<serde_json::Value> {
require_state_grant(plugin_id, grants, false)?;
let rows = store.sidecar_scan(plugin_id, graph, prefix, limit)?;
Ok(serde_json::json!({
"keys": rows.iter().map(|(k, v)| serde_json::json!({"key": k, "value": v})).collect::<Vec<_>>(),
}))
}
fn require_state_grant(
plugin_id: &str,
grants: &[scc_plugin_api::Permission],
write: bool,
) -> crate::Result<()> {
let need = if write {
scc_plugin_api::Permission::StateWrite
} else {
scc_plugin_api::Permission::StateRead
};
if grants.contains(&need) {
Ok(())
} else {
Err(crate::EngineError::Other(format!(
"permission denied: plugin {plugin_id} lacks {}",
need.as_str()
)))
}
}
pub fn import_plugin_evidence(
root: &Path,
plugin_id: &str,
file: &str,
) -> crate::Result<scc_indexer::adapters::ImportReport> {
let config = crate::workspace::load_config(root)?;
let ap = crate::plugins::active(root, &config);
let plug = ap
.plugins
.iter()
.find(|p| {
p.manifest.id == plugin_id
&& (p.manifest.extensions.iter().any(|e| e.extension_type == "evidence-provider")
|| p.manifest.operations.iter().any(|o| o == "evidence.import"))
})
.cloned()
.ok_or_else(|| {
crate::EngineError::Other(format!(
"unknown import format '{plugin_id}' (use scip, ccg, gitnexus, beads, cbm, hindsight, tracelayer, or a plugin id declaring evidence-provider)"
))
})?;
let out = scc_plugin_host::call(
&plug,
"evidence.import",
serde_json::json!({"file": file}),
None,
)
.map_err(|e| crate::EngineError::Other(format!("plugin {plugin_id} evidence.import: {e}")))?;
let batch = out.get("batch").cloned().unwrap_or(serde_json::json!({}));
let store = crate::workspace::open_store(root)?;
let committed =
crate::plugins::commit_contribution(&store, plugin_id, &batch)?;
store
.bump_epoch(scc_store::ModelEpochKind::Evidence)
?;
crate::index::recompile(&store)?;
Ok(scc_indexer::adapters::ImportReport {
symbols: committed.get("entities").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
calls: committed.get("relationships").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
imports: committed.get("evidence").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
errors: committed.get("diagnostics").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
})
}