1use std::io::Write;
7use std::path::{Path, PathBuf};
8
9pub fn lessons_add(root: &Path, text: &str) -> crate::Result<(String, PathBuf)> {
11 let dir = crate::workspace::scc_dir(root);
12 std::fs::create_dir_all(&dir)?;
13 let path = dir.join("lessons.jsonl");
14 let n = std::fs::read_to_string(&path)
15 .map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
16 .unwrap_or(0);
17 let id = format!("lesson-{}", n + 1);
18 let record = serde_json::json!({
19 "id": id,
20 "text": text,
21 "created_at": scc_core::now_rfc3339(),
22 });
23 let mut f = std::fs::OpenOptions::new()
24 .create(true)
25 .append(true)
26 .open(&path)
27 .map_err(|e| crate::EngineError::Other(format!("lessons: {e}")))?;
28 writeln!(f, "{record}").map_err(|e| crate::EngineError::Other(format!("lessons: {e}")))?;
29 Ok((id, path))
30}
31
32pub fn lessons_list(root: &Path, limit: usize) -> crate::Result<Vec<(String, Vec<String>)>> {
34 let store = crate::workspace::open_store(root)?;
35 Ok(scc_indexer::adapters::hindsight::lessons(&store, limit))
36}
37
38pub fn beads(root: &Path, limit: usize) -> crate::Result<Vec<String>> {
40 Ok(scc_indexer::adapters::beads::active_beads(root, limit))
41}
42
43pub fn import_evidence(root: &Path, format: &str, file: &str) -> crate::Result<scc_indexer::adapters::ImportReport> {
45 let store = crate::workspace::open_store(root)?;
46 let report = match format {
47 "scip" => scc_indexer::adapters::import_scip(&store, std::path::Path::new(file)),
48 "ccg" => scc_indexer::adapters::import_ccg(&store, std::path::Path::new(file)),
49 "gitnexus" => scc_indexer::adapters::gitnexus::import_gitnexus(&store, std::path::Path::new(file))
50 .map(|r| scc_indexer::adapters::ImportReport {
51 symbols: r.symbols,
52 calls: r.edges,
53 imports: 0,
54 errors: r.errors,
55 }),
56 "beads" => scc_indexer::adapters::beads::import_beads(&store, std::path::Path::new(file))
57 .map(|r| scc_indexer::adapters::ImportReport {
58 symbols: r.tasks,
59 calls: r.dependencies,
60 imports: r.active,
61 errors: r.errors,
62 }),
63 "cbm" => scc_indexer::adapters::cbm::import_cbm(&store, std::path::Path::new(file))
64 .map(|r| scc_indexer::adapters::ImportReport {
65 symbols: r.symbols,
66 calls: r.relationships,
67 imports: 0,
68 errors: r.errors,
69 }),
70 "hindsight" => scc_indexer::adapters::hindsight::import_hindsight(&store, std::path::Path::new(file))
71 .map(|r| scc_indexer::adapters::ImportReport {
72 symbols: r.lessons,
73 calls: 0,
74 imports: 0,
75 errors: r.errors,
76 }),
77 "tracelayer" => scc_indexer::adapters::tracelayer::import_tracelayer(&store, std::path::Path::new(file))
78 .map(|r| scc_indexer::adapters::ImportReport {
79 symbols: r.requirements + r.implementations + r.tests + r.decisions,
80 calls: r.relationships,
81 imports: r.work_items,
82 errors: r.errors,
83 }),
84 other => {
85 return Err(crate::EngineError::Other(format!(
86 "unknown import format '{other}' (use scip, ccg, gitnexus, beads, cbm, hindsight, or tracelayer)"
87 )))
88 }
89 }
90 .map_err(crate::EngineError::Other)?;
91 store.bump_epoch(scc_store::ModelEpochKind::Evidence)?;
92 crate::index::recompile(&store)?;
93 Ok(report)
94}
95
96pub fn runtime_edges(root: &Path) -> crate::Result<Vec<scc_indexer::runtime::RuntimeEdge>> {
98 let store = crate::workspace::open_store(root)?;
99 scc_indexer::runtime::runtime_edges(&store).map_err(crate::EngineError::Other)
100}
101
102pub fn ingest_runtime(root: &Path, body: &str) -> crate::Result<()> {
104 let store = crate::workspace::open_store(root)?;
105 if body.contains("resourceSpans") {
106 scc_indexer::runtime::ingest_otlp_json(&store, body)
107 .map_err(crate::EngineError::Other)?;
108 return Ok(());
109 }
110 scc_indexer::runtime::ingest_simple_edges(&store, body)
111 .map_err(crate::EngineError::Other)?;
112 Ok(())
113}
114
115pub fn reconcile(root: &Path) -> crate::Result<scc_indexer::runtime::Reconciliation> {
117 let store = crate::workspace::open_store(root)?;
118 scc_indexer::runtime::reconcile(&store).map_err(crate::EngineError::Other)
119}
120
121pub fn embeddings_build(root: &Path) -> crate::Result<serde_json::Value> {
123 use scc_indexer::embed::EmbedConfig;
124 let store = crate::workspace::open_store(root)?;
125 let config = crate::workspace::load_config(root)?;
126 if !config.inference.enabled {
127 return Err(crate::EngineError::Other(
128 "inference is disabled — set `inference.enabled: true` in .scc/config.yaml".into(),
129 ));
130 }
131 let cfg = EmbedConfig::from_config(&config.inference);
132 if cfg.is_remote() && !config.security.allow_remote_models {
133 return Err(crate::EngineError::Other(
134 "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(),
135 ));
136 }
137 if store.snapshot_status()?.is_none() {
138 return Err(crate::EngineError::Other("not indexed — run `scc index` first".into()));
139 }
140 let n = scc_indexer::embed::embed_repository(&store, &cfg).map_err(crate::EngineError::Other)?;
141 store.cache_clear()?;
142 Ok(serde_json::json!({"stored": n, "model": cfg.model}))
143}
144
145pub fn embeddings_get(store: &scc_store::Store, entity_id: &str) -> crate::Result<serde_json::Value> {
147 match store.get_embedding(entity_id)? {
148 Some((v, model)) => Ok(serde_json::json!({"id": entity_id, "model": model, "dims": v.len(), "vector": v})),
149 None => Ok(serde_json::Value::Null),
150 }
151}
152
153pub fn embeddings_status(store: &scc_store::Store) -> crate::Result<serde_json::Value> {
155 let count = store.embedding_count()?;
156 Ok(serde_json::json!({"embeddings": count}))
157}
158
159pub fn external_docs(root: &Path, dependency: &str) -> crate::Result<String> {
161 let config = crate::workspace::load_config(root)?;
162 if config.integrations.context7_command.is_empty() {
163 return Err(crate::EngineError::Other(
164 "Context7 is not configured — set integrations.context7_command in .scc/config.yaml".into(),
165 ));
166 }
167 let mut client = scc_indexer::adapters::context7::start(&config.integrations.context7_command, root)
168 .map_err(crate::EngineError::Other)?;
169 client.docs_for(dependency).map_err(crate::EngineError::Other)
170}
171
172pub fn plugin_state_get(
177 store: &scc_store::Store,
178 plugin_id: &str,
179 grants: &[scc_plugin_api::Permission],
180 key: &str,
181) -> crate::Result<serde_json::Value> {
182 require_state_grant(plugin_id, grants, false)?;
183 Ok(store.plugin_state_get(plugin_id, key)?.into())
184}
185
186pub fn plugin_state_put(
189 store: &scc_store::Store,
190 plugin_id: &str,
191 grants: &[scc_plugin_api::Permission],
192 key: &str,
193 value: &str,
194) -> crate::Result<serde_json::Value> {
195 require_state_grant(plugin_id, grants, true)?;
196 store.plugin_state_put(plugin_id, key, value)?;
197 Ok(serde_json::json!({"ok": true}))
198}
199
200pub fn plugin_state_delete(
203 store: &scc_store::Store,
204 plugin_id: &str,
205 grants: &[scc_plugin_api::Permission],
206 key: &str,
207) -> crate::Result<serde_json::Value> {
208 require_state_grant(plugin_id, grants, true)?;
209 store.plugin_state_delete(plugin_id, key)?;
210 Ok(serde_json::json!({"ok": true}))
211}
212
213pub fn plugin_state_scan(
216 store: &scc_store::Store,
217 plugin_id: &str,
218 grants: &[scc_plugin_api::Permission],
219 prefix: &str,
220 limit: usize,
221) -> crate::Result<serde_json::Value> {
222 require_state_grant(plugin_id, grants, false)?;
223 let rows = store.plugin_state_scan(plugin_id, prefix, limit)?;
224 Ok(serde_json::json!({
225 "keys": rows.iter().map(|(k, v)| serde_json::json!({"key": k, "value": v})).collect::<Vec<_>>(),
226 }))
227}
228
229fn require_state_grant(
231 plugin_id: &str,
232 grants: &[scc_plugin_api::Permission],
233 write: bool,
234) -> crate::Result<()> {
235 let need = if write {
236 scc_plugin_api::Permission::StateWrite
237 } else {
238 scc_plugin_api::Permission::StateRead
239 };
240 if grants.contains(&need) {
241 Ok(())
242 } else {
243 Err(crate::EngineError::Other(format!(
244 "permission denied: plugin {plugin_id} lacks {}",
245 need.as_str()
246 )))
247 }
248}