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 import_plugin_evidence(root, other, file);
86 }
87 }
88 .map_err(crate::EngineError::Other)?;
89 store.bump_epoch(scc_store::ModelEpochKind::Evidence)?;
90 crate::index::recompile(&store)?;
91 Ok(report)
92}
93
94pub fn runtime_edges(root: &Path) -> crate::Result<Vec<scc_indexer::runtime::RuntimeEdge>> {
96 let store = crate::workspace::open_store(root)?;
97 scc_indexer::runtime::runtime_edges(&store).map_err(crate::EngineError::Other)
98}
99
100pub fn ingest_runtime(root: &Path, body: &str) -> crate::Result<()> {
102 let store = crate::workspace::open_store(root)?;
103 if body.contains("resourceSpans") {
104 scc_indexer::runtime::ingest_otlp_json(&store, body)
105 .map_err(crate::EngineError::Other)?;
106 } else {
107 scc_indexer::runtime::ingest_simple_edges(&store, body)
108 .map_err(crate::EngineError::Other)?;
109 }
110 ingest_plugin_runtime_edges(root, &store, body);
114 Ok(())
115}
116
117pub fn ingest_plugin_runtime_edges(
124 root: &Path,
125 store: &scc_store::Store,
126 body: &str,
127) {
128 let config = match crate::workspace::load_config(root) {
129 Ok(c) => c,
130 Err(_) => return,
131 };
132 let ap = crate::plugins::active(root, &config);
133 for plug in &ap.plugins {
134 let is_provider = plug.manifest.extensions.iter().any(|e| e.extension_type == "runtime-evidence")
135 || plug.manifest.operations.iter().any(|o| o == "runtime.evidence");
136 if !is_provider {
137 continue;
138 }
139 let out = match scc_plugin_host::call(
140 plug,
141 "runtime.evidence",
142 serde_json::json!({"body": body}),
143 None,
144 ) {
145 Ok(v) => v,
146 Err(_) => continue,
147 };
148 let edges = out.get("edges").and_then(|v| v.as_array()).cloned().unwrap_or_default();
149 if edges.is_empty() {
150 continue;
151 }
152 let payload = serde_json::Value::Array(edges);
153 let text = payload.to_string();
154 let _ = scc_indexer::runtime::ingest_simple_edges(store, &text);
155 }
156}
157
158pub fn reconcile(root: &Path) -> crate::Result<scc_indexer::runtime::Reconciliation> {
160 let store = crate::workspace::open_store(root)?;
161 scc_indexer::runtime::reconcile(&store).map_err(crate::EngineError::Other)
162}
163
164pub fn embeddings_build(root: &Path) -> crate::Result<serde_json::Value> {
166 use scc_indexer::embed::EmbedConfig;
167 let store = crate::workspace::open_store(root)?;
168 let config = crate::workspace::load_config(root)?;
169 if !config.inference.enabled {
170 return Err(crate::EngineError::Other(
171 "inference is disabled — set `inference.enabled: true` in .scc/config.yaml".into(),
172 ));
173 }
174 let cfg = EmbedConfig::from_config(&config.inference);
175 if cfg.is_remote() && !config.security.allow_remote_models {
176 return Err(crate::EngineError::Other(
177 "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(),
178 ));
179 }
180 if store.snapshot_status()?.is_none() {
181 return Err(crate::EngineError::Other("not indexed — run `scc index` first".into()));
182 }
183 let n = scc_indexer::embed::embed_repository(&store, &cfg).map_err(crate::EngineError::Other)?;
184 store.cache_clear()?;
185 Ok(serde_json::json!({"stored": n, "model": cfg.model}))
186}
187
188pub fn embeddings_get(store: &scc_store::Store, entity_id: &str) -> crate::Result<serde_json::Value> {
190 match store.get_embedding(entity_id)? {
191 Some((v, model)) => Ok(serde_json::json!({"id": entity_id, "model": model, "dims": v.len(), "vector": v})),
192 None => Ok(serde_json::Value::Null),
193 }
194}
195
196pub fn embeddings_status(store: &scc_store::Store) -> crate::Result<serde_json::Value> {
198 let count = store.embedding_count()?;
199 Ok(serde_json::json!({"embeddings": count}))
200}
201
202pub fn external_docs(root: &Path, dependency: &str) -> crate::Result<String> {
204 let config = crate::workspace::load_config(root)?;
205 if config.integrations.context7_command.is_empty() {
206 return Err(crate::EngineError::Other(
207 "Context7 is not configured — set integrations.context7_command in .scc/config.yaml".into(),
208 ));
209 }
210 let mut client = scc_indexer::adapters::context7::start(&config.integrations.context7_command, root)
211 .map_err(crate::EngineError::Other)?;
212 client.docs_for(dependency).map_err(crate::EngineError::Other)
213}
214
215pub fn plugin_state_get(
220 store: &scc_store::Store,
221 plugin_id: &str,
222 grants: &[scc_plugin_api::Permission],
223 key: &str,
224) -> crate::Result<serde_json::Value> {
225 require_state_grant(plugin_id, grants, false)?;
226 Ok(store.plugin_state_get(plugin_id, key)?.into())
227}
228
229pub fn plugin_state_put(
232 store: &scc_store::Store,
233 plugin_id: &str,
234 grants: &[scc_plugin_api::Permission],
235 key: &str,
236 value: &str,
237) -> crate::Result<serde_json::Value> {
238 require_state_grant(plugin_id, grants, true)?;
239 store.plugin_state_put(plugin_id, key, value)?;
240 Ok(serde_json::json!({"ok": true}))
241}
242
243pub fn plugin_state_delete(
246 store: &scc_store::Store,
247 plugin_id: &str,
248 grants: &[scc_plugin_api::Permission],
249 key: &str,
250) -> crate::Result<serde_json::Value> {
251 require_state_grant(plugin_id, grants, true)?;
252 store.plugin_state_delete(plugin_id, key)?;
253 Ok(serde_json::json!({"ok": true}))
254}
255
256pub fn plugin_state_scan(
259 store: &scc_store::Store,
260 plugin_id: &str,
261 grants: &[scc_plugin_api::Permission],
262 prefix: &str,
263 limit: usize,
264) -> crate::Result<serde_json::Value> {
265 require_state_grant(plugin_id, grants, false)?;
266 let rows = store.plugin_state_scan(plugin_id, prefix, limit)?;
267 Ok(serde_json::json!({
268 "keys": rows.iter().map(|(k, v)| serde_json::json!({"key": k, "value": v})).collect::<Vec<_>>(),
269 }))
270}
271
272pub fn sidecar_put(
278 store: &scc_store::Store,
279 plugin_id: &str,
280 grants: &[scc_plugin_api::Permission],
281 graph: &str,
282 key: &str,
283 value: &str,
284) -> crate::Result<serde_json::Value> {
285 require_state_grant(plugin_id, grants, true)?;
286 store.sidecar_put(plugin_id, graph, key, value)?;
287 Ok(serde_json::json!({"ok": true}))
288}
289
290pub fn sidecar_get(
292 store: &scc_store::Store,
293 plugin_id: &str,
294 grants: &[scc_plugin_api::Permission],
295 graph: &str,
296 key: &str,
297) -> crate::Result<serde_json::Value> {
298 require_state_grant(plugin_id, grants, false)?;
299 Ok(store.sidecar_get(plugin_id, graph, key)?.into())
300}
301
302pub fn sidecar_scan(
304 store: &scc_store::Store,
305 plugin_id: &str,
306 grants: &[scc_plugin_api::Permission],
307 graph: &str,
308 prefix: &str,
309 limit: usize,
310) -> crate::Result<serde_json::Value> {
311 require_state_grant(plugin_id, grants, false)?;
312 let rows = store.sidecar_scan(plugin_id, graph, prefix, limit)?;
313 Ok(serde_json::json!({
314 "keys": rows.iter().map(|(k, v)| serde_json::json!({"key": k, "value": v})).collect::<Vec<_>>(),
315 }))
316}
317
318fn require_state_grant(
320 plugin_id: &str,
321 grants: &[scc_plugin_api::Permission],
322 write: bool,
323) -> crate::Result<()> {
324 let need = if write {
325 scc_plugin_api::Permission::StateWrite
326 } else {
327 scc_plugin_api::Permission::StateRead
328 };
329 if grants.contains(&need) {
330 Ok(())
331 } else {
332 Err(crate::EngineError::Other(format!(
333 "permission denied: plugin {plugin_id} lacks {}",
334 need.as_str()
335 )))
336 }
337}
338
339pub fn import_plugin_evidence(
349 root: &Path,
350 plugin_id: &str,
351 file: &str,
352) -> crate::Result<scc_indexer::adapters::ImportReport> {
353 let config = crate::workspace::load_config(root)?;
354 let ap = crate::plugins::active(root, &config);
355 let plug = ap
356 .plugins
357 .iter()
358 .find(|p| {
359 p.manifest.id == plugin_id
360 && (p.manifest.extensions.iter().any(|e| e.extension_type == "evidence-provider")
361 || p.manifest.operations.iter().any(|o| o == "evidence.import"))
362 })
363 .cloned()
364 .ok_or_else(|| {
365 crate::EngineError::Other(format!(
366 "unknown import format '{plugin_id}' (use scip, ccg, gitnexus, beads, cbm, hindsight, tracelayer, or a plugin id declaring evidence-provider)"
367 ))
368 })?;
369 let out = scc_plugin_host::call(
370 &plug,
371 "evidence.import",
372 serde_json::json!({"file": file}),
373 None,
374 )
375 .map_err(|e| crate::EngineError::Other(format!("plugin {plugin_id} evidence.import: {e}")))?;
376 let batch = out.get("batch").cloned().unwrap_or(serde_json::json!({}));
377 let store = crate::workspace::open_store(root)?;
378 let committed =
379 crate::plugins::commit_contribution(&store, plugin_id, &batch)?;
380 store
381 .bump_epoch(scc_store::ModelEpochKind::Evidence)
382 ?;
383 crate::index::recompile(&store)?;
384 Ok(scc_indexer::adapters::ImportReport {
385 symbols: committed.get("entities").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
386 calls: committed.get("relationships").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
387 imports: committed.get("evidence").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
388 errors: committed.get("diagnostics").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
389 })
390}