1use serde_json::Value;
9
10pub fn invoke(
12 root: &std::path::Path,
13 operation: &str,
14 input: Value,
15) -> crate::Result<Value> {
16 let store = crate::workspace::open_store(root)?;
17 let config = crate::workspace::load_config(root)?;
18 let stale = crate::workspace::stale_paths(&store)?;
19 let engine = crate::workspace::open_engine(&store, &config, stale)?;
20 let ctx = engine.context();
21 let out = match operation {
22 "workspace.status" => {
23 let s = status_value(&store)?;
24 serde_json::to_value(&s)?
25 }
26 "workspace.languages" => Value::String(scc_core::support_matrix_markdown()),
27 "context.overview" => serde_json::to_value(ctx.overview()?)?,
28 "context.atlas" => {
29 let budget: Option<usize> = input.get("budget").and_then(|v| v.as_u64()).map(|v| v as usize);
30 let full: bool = input.get("full").and_then(|v| v.as_bool()).unwrap_or(false);
31 let unbounded: bool = input.get("unbounded").and_then(|v| v.as_bool()).unwrap_or(false);
32 let model: bool = input.get("model").and_then(|v| v.as_bool()).unwrap_or(false);
33 if model {
36 let scope = if full {
37 scc_context::atlas::AtlasScope::Full
38 } else {
39 scc_context::atlas::AtlasScope::Production
40 };
41 let pack = ctx.atlas(budget, full, unbounded)?;
42 serde_json::json!({"pack": pack, "model": ctx.atlas_model(scope)?})
43 } else {
44 serde_json::to_value(ctx.atlas(budget, full, unbounded)?)?
45 }
46 }
47 "context.startup" => {
48 let want_model = input.get("model").and_then(|v| v.as_bool()).unwrap_or(false);
49 let req: scc_api::StartupRequest = serde_json::from_value(input).unwrap_or(scc_api::StartupRequest { budget: None });
50 let (startup, text) = ctx.startup(&req)?;
53 let budget = scc_context::startup::allocate_startup_budget(&ctx.engine.ctx(), req.budget);
54 let (sections, notes) = crate::plugins::startup_sections(root, &config);
59 let mut text = text;
60 text.push_str(§ions);
61 for n in notes {
62 text.push_str(&format!("\n(startup section skipped: {n})\n"));
63 }
64 let mut out = serde_json::json!({"text": text, "budget": budget, "artifact": startup.artifact});
67 if want_model {
68 out["model"] = serde_json::to_value(ctx.startup_model(&req)?)?;
69 }
70 out
71 }
72 "context.task" => {
73 let req: scc_api::TaskContextRequest = serde_json::from_value(input)?;
74 let (scorer, reranker) = crate::inference::rankers(&store, &config, &req.goal);
75 let scorer_trait: Option<&dyn scc_context::rank::SemanticScorer> =
76 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
77 let reranker_trait: Option<&dyn scc_context::rank::Reranker> =
78 reranker.as_ref().map(|r| r as &dyn scc_context::rank::Reranker);
79 serde_json::to_value(crate::task::build_task_context(&engine, &config, root, &req, scorer_trait, reranker_trait)?)?
80 }
81 "context.task_pack" => {
82 let req: scc_api::TaskContextRequest = serde_json::from_value(input)?;
83 let (scorer, reranker) = crate::inference::rankers(&store, &config, &req.goal);
84 let scorer_trait: Option<&dyn scc_context::rank::SemanticScorer> =
85 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
86 let reranker_trait: Option<&dyn scc_context::rank::Reranker> =
87 reranker.as_ref().map(|r| r as &dyn scc_context::rank::Reranker);
88 serde_json::to_value(crate::task::build_enriched_task_pack(&engine, &config, root, &req, scorer_trait, reranker_trait)?)?
89 }
90 "context.task_delta" => {
91 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
92 let budget: usize = input.get("budget").and_then(|v| v.as_u64()).map(|v| v as usize).unwrap_or(scc_core::ContextBudget::default().task_delta);
93 let record = input.get("record_visibility").and_then(|v| v.as_bool()).unwrap_or(true);
97 let (scorer, _) = crate::inference::rankers(&store, &config, goal);
98 let semantic: Option<&dyn scc_context::rank::SemanticScorer> =
99 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
100 let (delta, ids) = ctx.task_delta(goal, budget, semantic)?;
101 if record {
102 ctx.record_task_delta_ids(&ids);
103 }
104 serde_json::json!({"delta": delta, "delta_ids": ids})
105 }
106 "context.subagent" => {
107 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
108 let files: Vec<String> = input.get("files").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();
109 let symbols: Vec<String> = input.get("symbols").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();
110 let budget = input.get("budget").and_then(|v| v.as_u64()).map(|b| b as usize);
111 serde_json::to_value(ctx.subagent(goal, &files, &symbols, budget)?)?
112 }
113 "context.compress" => {
114 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
117 let budget = input.get("budget").and_then(|v| v.as_u64()).map(|b| b as usize);
118 let (scorer, reranker) = crate::inference::rankers(&store, &config, goal);
119 let scorer_trait: Option<&dyn scc_context::rank::SemanticScorer> =
120 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
121 let reranker_trait: Option<&dyn scc_context::rank::Reranker> =
122 reranker.as_ref().map(|r| r as &dyn scc_context::rank::Reranker);
123 let pack = crate::task::build_enriched_task_pack(&engine, &config, root, &scc_api::TaskContextRequest { goal: goal.into(), files: vec![], symbols: vec![], budget, hook: false, record_visibility: true }, scorer_trait, reranker_trait)?;
124 serde_json::to_value(pack)?
125 }
126 "context.component" | "context.flow" | "context.impact" | "context.verify" | "context.structural" | "source.structural" | "surface.build" | "surface.compile" | "surface.global" | "surface.task" | "surface.render" => {
127 invoke_context(&ctx, &store, &config, operation, input)?
128 }
129 "graph.query" => {
130 let req: scc_api::QueryRequest = serde_json::from_value(input)?;
131 let hit = crate::graph::query(&store, &req)?;
132 serde_json::json!({
133 "entities": hit.entities,
134 "symbols": hit.symbols.iter().map(|(n, s, k, f, l)| serde_json::json!({"name": n, "signature": s, "kind": k, "file": f, "line": l})).collect::<Vec<_>>(),
135 })
136 }
137 "graph.traverse" => {
138 let req: scc_api::TraverseRequest = serde_json::from_value(input)?;
139 let cc = ctx.engine.ctx();
140 let (entities, relationships) = crate::graph::traverse(&cc, &req)?;
141 serde_json::json!({"entities": entities, "relationships": relationships, "trusted_only": req.trusted_only})
142 }
143 "graph.entities" | "architecture.components" => serde_json::to_value(crate::graph::components(&store)?)?,
144 "graph.flows" | "architecture.flows" => serde_json::to_value(crate::graph::flows(&store)?)?,
145 "graph.relationships" => {
146 let subject = input.get("subject").and_then(|v| v.as_str());
147 let predicate = input.get("predicate").and_then(|v| v.as_str());
148 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
149 serde_json::to_value(crate::graph::relationships(&store, subject, predicate, limit)?)?
150 }
151 "graph.search" => {
152 let q = input.get("query").and_then(|v| v.as_str()).unwrap_or("");
153 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
154 let mut entities = store.search_entities(q, limit)?;
155 if entities.is_empty() { entities = store.search_entities_like(q, limit)?; }
156 serde_json::to_value(entities)?
157 }
158 "graph.search_symbols" => {
159 let q = input.get("query").and_then(|v| v.as_str()).unwrap_or("");
160 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
161 let mut symbols = store.search_symbols(q, limit)?;
162 if symbols.is_empty() { symbols = store.search_symbols_like(q, limit)?; }
163 serde_json::json!({"symbols": symbols.iter().map(|(n, s, k, f, l)| serde_json::json!({"name": n, "signature": s, "kind": k, "file": f, "line": l})).collect::<Vec<_>>()})
164 }
165 "evidence.get" => {
166 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
167 serde_json::to_value(store.get_evidence(id)?)?
168 }
169 "evidence.list" | "evidence.search" => {
170 let path = input.get("path").and_then(|v| v.as_str());
171 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(200) as usize;
172 let mut out = match path {
173 Some(p) => store.evidence_for_path(p)?,
174 None => store.all_evidence()?,
175 };
176 out.truncate(limit.max(1));
177 serde_json::to_value(out)?
178 }
179 "runtime.signatures" => {
180 let rows = store.trace_signatures()?;
181 serde_json::json!({"signatures": rows.iter().map(|(sig, count, lat, err, last)| serde_json::json!({"signature": sig, "count": count, "latency_ms": lat, "errors": err, "last_observed": last})).collect::<Vec<_>>()})
182 }
183 "integrations.describe" => {
184 let name = input.get("name").and_then(|v| v.as_str()).unwrap_or("");
185 let all = crate::integrations::list(root)?;
186 match all.into_iter().find(|(n, _)| n == name) {
187 Some((n, scope)) => serde_json::json!({"name": n, "scope": scope}),
188 None => serde_json::json!({"error": format!("unknown integration '{name}'")}),
189 }
190 }
191 "index.full" => serde_json::to_value(crate::index::full(root, &config)?)?,
192 "index.refresh" | "index.paths" => {
193 let req: scc_api::IndexPathsRequest = serde_json::from_value(input)?;
194 serde_json::to_value(crate::index::refresh_paths(root, &config, &req.paths)?)?
195 }
196 "history.revisions" | "history.list" => serde_json::to_value(crate::history::revisions(&store)?)?,
197 "history.diff" => {
198 let req: scc_api::DiffRequest = serde_json::from_value(input)?;
199 serde_json::to_value(crate::history::diff(&store, req.from, req.to)?)?
200 }
201 "model.get" | "model.components" | "model.flows" | "model.invariants" => serde_json::to_value(crate::exports::model_get(&store)?)?,
202 "context.external_docs" | "context.docs" => {
203 let dep = input.get("dependency").and_then(|v| v.as_str()).unwrap_or("");
204 Value::String(crate::state::external_docs(root, dep)?)
205 }
206 "embeddings.build" => crate::state::embeddings_build(root)?,
207 "embeddings.get" => {
208 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
209 crate::state::embeddings_get(&store, id)?
210 }
211 "embeddings.status" => crate::state::embeddings_status(&store)?,
212 "export.system_ir" => {
213 let req: scc_api::ExportRequest = serde_json::from_value(input).unwrap_or(scc_api::ExportRequest { format: "system-ir.json".into() });
214 export_value(&store, root, &req.format)?
215 }
216 "export.system_ir_jsonl" => export_value(&store, root, "system-ir.jsonl")?,
217 "export.ccg" => export_value(&store, root, "ccg")?,
218 "export.flow_graphs" => export_value(&store, root, "flow-graphs.json")?,
219 "export.snap" | "export.capsule" => export_value(&store, root, "capsule.md")?,
220 "operations.list" => {
221 serde_json::json!({"operations": crate::ops::ids(), "api_version": scc_api::API_VERSION})
222 }
223 "operations.capabilities" => {
224 serde_json::to_value(crate::ops::capabilities())?
225 }
226 "operations.schema" => {
227 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
228 serde_json::to_value(crate::ops::input_schema(id).ok_or_else(|| {
229 crate::EngineError::Other(format!("unknown operation '{id}' (see operations.list)"))
230 })?)?
231 }
232 "operations.describe" => {
233 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
234 match crate::ops::describe(id) {
235 Some(d) => serde_json::to_value(d)?,
236 None => return Err(crate::EngineError::Other(format!("unknown operation '{id}' (see operations.list)"))),
237 }
238 }
239 "workspace.init" => {
240 let dir = crate::workspace::scc_dir(root);
241 std::fs::create_dir_all(&dir)?;
242 let cfg_path = crate::workspace::config_path(root);
243 if !cfg_path.exists() {
244 std::fs::write(&cfg_path, scc_indexer::Config::default_yaml())?;
245 }
246 crate::workspace::ensure_scc_ignored(root);
247 serde_json::json!({"initialized": dir.to_string_lossy(), "config": cfg_path.to_string_lossy()})
248 }
249 "workspace.state_path" => Value::String(crate::workspace::state_dir(root).to_string_lossy().into()),
250 "workspace.scan" => {
251 let path = input.get("path").and_then(|v| v.as_str());
252 serde_json::to_value(crate::status::scan(root, &config, path)?)?
253 }
254 "workspace.session" => serde_json::to_value(crate::workspace::open_session(&store, &config)?)?,
255 "workspace.session_check" => {
256 let session: crate::workspace::Session = serde_json::from_value(input.get("session").cloned().unwrap_or(serde_json::Value::Null))?;
257 serde_json::json!({"current": crate::workspace::session_is_current(&store, &config, &session)?})
258 }
259 "index.status" => {
260 let s = status_value(&store)?;
261 serde_json::to_value(&s)?
262 }
263 "resolution.run" => serde_json::to_value(crate::index::resolve_and_recompile(root)?)?,
264 "graph.recompile" => {
265 let r = crate::index::recompile_with_signals(root, &config, &store)?;
266 serde_json::json!({
267 "components": r.components,
268 "flows": r.flows,
269 "invariants": r.invariants,
270 "drift": r.drift,
271 "boundaries": r.boundaries,
272 })
273 },
274 "graph.explain" => {
275 let subject = input.get("subject").and_then(|v| v.as_str()).unwrap_or("");
276 let predicate = input.get("predicate").and_then(|v| v.as_str()).unwrap_or("");
277 let object = input.get("object").and_then(|v| v.as_str()).unwrap_or("");
278 let cc = ctx.engine.ctx();
279 crate::graph::explain(&cc, subject, predicate, object)
280 }
281 "graph.entity.get" | "graph.entity" => {
282 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
283 let found = store.all_entities()?.into_iter().find(|e| e.id == id);
284 match found {
285 None => serde_json::json!({"entity": null, "trusted": false, "reason": "unknown id"}),
286 Some(e) => {
287 let cc = ctx.engine.ctx();
288 match cc.view.entity(&e.id) {
289 Some(_) => serde_json::json!({"entity": e, "trusted": true, "reason": null}),
290 None => serde_json::json!({"entity": e, "trusted": false, "reason": "hidden by TrustedGraphView (stale evidence or below trust floor)"}),
291 }
292 }
293 }
294 }
295 "architecture.drift" | "model.drift" => serde_json::to_value(crate::misc::drift(&store)?)?,
296 "architecture.cochange" => {
297 let min = input.get("min_commits").and_then(|v| v.as_u64()).unwrap_or(2) as u32;
298 let (pairs, enriched) = crate::misc::cochange(root, min)?;
299 serde_json::json!({"pairs": pairs, "enriched": enriched})
300 }
301 "integrity.invariants" | "architecture.invariants" => serde_json::to_value(crate::misc::check_invariants(&store)?)?,
302 "integrity.ci" => {
303 let max = input.get("max_severity").and_then(|v| v.as_str()).unwrap_or("medium");
304 let violations = crate::misc::check_invariants(&store)?;
305 let (ok, lines) = crate::misc::ci_check(&store, &violations, max)?;
306 serde_json::json!({"ok": ok, "lines": lines})
307 }
308 "integrations.list" | "integration.list" => serde_json::to_value(crate::integrations::list(root)?)?,
309 "integrations.doctor" | "integration.doctor" => {
310 let deep = input.get("deep").and_then(|v| v.as_bool()).unwrap_or(false);
311 let network = input.get("network").and_then(|v| v.as_bool()).unwrap_or(false);
312 serde_json::to_value(crate::integrations::doctor_report(&store, &config, root, deep, network)?)?
313 }
314 "lessons.add" => {
315 let text = input.get("text").and_then(|v| v.as_str()).unwrap_or("");
316 let (id, path) = crate::state::lessons_add(root, text)?;
317 serde_json::json!({"id": id, "path": path.to_string_lossy()})
318 }
319 "lessons.list" => {
320 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
321 serde_json::to_value(crate::state::lessons_list(root, limit)?)?
322 }
323 "beads.list" | "beads.active" => serde_json::to_value(crate::state::beads(root, 20)?)?,
324 "viewer.panels" => {
325 let (panels, notes) = crate::plugins::viewer_panels(root, &config);
329 serde_json::json!({"panels": panels, "skipped": notes})
330 }
331 "viewer.snapshot" => {
332 return Err(crate::EngineError::Other("viewer.snapshot is CLI-local browser capture; not an engine operation".into()));
333 }
334 "setup.claude" | "setup.detected" | "setup.codex" | "setup.opencode" | "setup.hermes" | "setup.omp" | "setup.pi" => {
335 return Err(crate::EngineError::Other("setup operations are CLI-local file installation (harness dirs, home directory); not engine operations".into()));
336 }
337 "snapshot.save" => {
338 let req: scc_api::SnapshotSaveRequest = serde_json::from_value(input)?;
339 serde_json::to_value(crate::misc::snapshot_save(root, &req.task, req.budget)?)?
340 }
341 "snapshot.get" => {
342 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
343 serde_json::to_value(crate::snapshots::get(&store, id)?)?
344 }
345 "snapshot.diff" => {
346 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
347 serde_json::to_value(crate::snapshots::diff(&store, id)?)?
348 }
349 "checkpoint.save" => serde_json::to_value(crate::checkpoint::capture(root)?)?,
350 "checkpoint.load" => serde_json::to_value(crate::checkpoint::load(root)?)?,
351 "system.stitch" => {
352 let members: Vec<std::path::PathBuf> = input.get("members")
353 .and_then(|v| v.as_array())
354 .map(|a| a.iter().filter_map(|s| s.as_str().map(std::path::PathBuf::from)).collect())
355 .unwrap_or_default();
356 let (members, stitches) = crate::systems::stitch(&members)?;
357 serde_json::json!({"members": members.iter().map(|m| &m.repo_id).collect::<Vec<_>>(), "stitches": stitches})
358 }
359 "import.scip" | "import.ccg" | "import.gitnexus" | "import.tracelayer" | "import.beads" | "import.hindsight" | "import.cbm" => {
360 let format = operation.trim_start_matches("import.");
361 let file = input.get("file").and_then(|v| v.as_str()).unwrap_or("");
362 let r = crate::state::import_evidence(root, format, file)?;
363 serde_json::json!({
364 "symbols": r.symbols,
365 "calls": r.calls,
366 "imports": r.imports,
367 "errors": r.errors,
368 })
369 }
370 _ if operation.starts_with("import.") => {
371 let format = operation.trim_start_matches("import.");
374 let file = input.get("file").and_then(|v| v.as_str()).unwrap_or("");
375 let r = crate::state::import_evidence(root, format, file)?;
376 serde_json::json!({
377 "symbols": r.symbols,
378 "calls": r.calls,
379 "imports": r.imports,
380 "errors": r.errors,
381 })
382 }
383 "export.diagram" | "diagram.render" => {
384 let format = input.get("format").and_then(|v| v.as_str()).unwrap_or("mermaid");
385 crate::exports::diagram(&store, format)?
386 }
387 "index.watch" => {
388 return Err(crate::EngineError::Other("index.watch is a streaming filesystem watch, not a request/response operation; run `scc watch`".into()));
389 }
390 "runtime.ingest" => {
391 let body = input.get("body").and_then(|v| v.as_str()).unwrap_or("");
392 crate::state::ingest_runtime(root, body)?;
393 serde_json::json!({"status": "accepted"})
394 }
395 "runtime.status" => serde_json::to_value(crate::state::runtime_edges(root)?)?,
396 "runtime.reconcile" => serde_json::to_value(crate::state::reconcile(root)?)?,
397 "ranking.important" | "surface.important" => {
398 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
399 let task = input.get("task").and_then(|v| v.as_str()).map(|s| s.to_string());
400 let component = input.get("component").and_then(|v| v.as_str()).map(|s| s.to_string());
401 let (entries, tasked) = ctx.important(limit, component.as_deref(), task.as_deref())?;
402 serde_json::json!({"entries": entries, "tasked": tasked})
403 }
404 "ranking.symbols" | "surface.rank" => {
405 let req: scc_api::RankRequest = serde_json::from_value(input)?;
406 let ranker = engine.ranking();
407 let mut ap = crate::plugins::active(root, &config);
408 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
409 let hooks = ranking_hooks_from_plugins(&mut ap, req.goal.as_deref().unwrap_or(""));
410 let out = ranker.symbols_with_hooks(&req, &hooks)?;
411 let mut v = serde_json::to_value(&out)?;
412 if !ap.diagnostics.is_empty() {
413 v["plugin_diagnostics"] = serde_json::to_value(&ap.diagnostics)?;
414 }
415 v
416 }
417 "ranking.candidates" => {
418 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
419 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
420 let mut ap = crate::plugins::active(root, &config);
421 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
422 let hooks = ranking_hooks_from_plugins(&mut ap, goal);
423 let cands = engine.ranking().candidates_with(goal, limit, &hooks)?;
424 serde_json::json!({"candidates": cands.iter().map(|c| serde_json::json!({"id": c.id, "kind": c.kind, "name": c.name, "score": c.score, "reason": c.reason})).collect::<Vec<_>>()})
425 }
426 "ranking.project_symbols" => {
427 let rows: Vec<(String, f64)> = match input.get("vector").and_then(|v| v.as_array()) {
431 Some(arr) => arr.iter().map(|r| (
432 r.get("id").and_then(|x| x.as_str()).unwrap_or("").to_string(),
433 r.get("score").and_then(|x| x.as_f64()).unwrap_or(0.0),
434 )).collect(),
435 None => match input.get("source").and_then(|v| v.as_str()) {
436 Some("global") => engine.ranking().pagerank_global()?,
437 _ => {
438 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
439 engine.ranking().pagerank_task(goal)?
440 }
441 },
442 };
443 let out = engine.ranking().project_symbols(&rows)?;
444 serde_json::json!({"symbols": out.iter().map(|(id, s)| serde_json::json!({"id": id, "score": s})).collect::<Vec<_>>()})
445 }
446 "ranking.features" => {
447 let req: scc_api::RankRequest = serde_json::from_value(input)?;
451 let ranker = engine.ranking();
452 let mut ap = crate::plugins::active(root, &config);
453 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
454 let hooks = ranking_hooks_from_plugins(&mut ap, req.goal.as_deref().unwrap_or(""));
455 let out = ranker.symbols_with_hooks(&req, &hooks)?;
456 serde_json::json!({"features": out.items.iter().map(|i| serde_json::json!({
457 "id": i.id, "position": i.position,
458 "task_ppr": i.features.task_ppr, "global_ppr": i.features.global_ppr,
459 "lexical": i.features.lexical, "semantic": i.features.semantic,
460 "confidence": i.features.confidence, "criticality": i.features.criticality,
461 "change_risk": i.features.change_risk, "novelty": i.features.novelty,
462 "specificity": i.specificity, "plugin_features": i.plugin_features,
463 })).collect::<Vec<_>>()})
464 }
465 "ranking.reference_graph" => {
466 let edges = engine.ranking().reference_graph()?;
467 serde_json::to_value(&edges)?
468 }
469 "ranking.universe" => {
470 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
474 let mut ap = crate::plugins::active(root, &config);
475 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
476 let hooks = ranking_hooks_from_plugins(&mut ap, goal);
477 let nodes = engine.ranking().universe_with(&hooks, goal)?;
478 serde_json::json!({"nodes": nodes.iter().map(|(id, k)| serde_json::json!({"id": id, "kind": k})).collect::<Vec<_>>()})
479 }
480 "ranking.edges" => {
481 let edges = engine.ranking().rank_edges()?;
482 serde_json::json!({"edges": edges.iter().map(|(s, p, o, w)| serde_json::json!({"subject": s, "predicate": p, "object": o, "weight": w})).collect::<Vec<_>>()})
483 }
484 "ranking.seeds" => {
485 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
489 let mut ap = crate::plugins::active(root, &config);
490 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
491 let hooks = ranking_hooks_from_plugins(&mut ap, goal);
492 let seeds = engine.ranking().seeds_with(goal, &hooks)?;
493 serde_json::json!({"seeds": seeds.iter().map(|x| serde_json::json!({"id": x.id, "kind": x.kind, "weight": x.weight})).collect::<Vec<_>>()})
494 }
495 "ranking.pagerank.global" | "ranking.global_vector" => {
496 let v = engine.ranking().pagerank_global()?;
498 serde_json::json!({"vector": v.iter().map(|(id, s)| serde_json::json!({"id": id, "score": s})).collect::<Vec<_>>()})
499 }
500 "ranking.pagerank.task" | "ranking.task_vector" => {
501 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
503 let v = engine.ranking().pagerank_task(goal)?;
504 serde_json::json!({"vector": v.iter().map(|(id, s)| serde_json::json!({"id": id, "score": s})).collect::<Vec<_>>()})
505 }
506 "ranking.final_importance" => {
507 let f = |k: &str| input.get(k).and_then(|v| v.as_f64()).unwrap_or(0.0);
508 let has_task = input.get("has_task").and_then(|v| v.as_bool()).unwrap_or(true);
509 let s = scc_context::pagerank::final_importance(f("task_ppr"), f("global_ppr"), f("lexical"), f("semantic"), f("confidence"), f("criticality"), f("change_risk"), f("novelty"), has_task);
510 serde_json::json!({"score": s})
511 }
512 "ranking.score_entries" => {
513 let rows: Vec<crate::ranking::ScoreRow> = input.get("entries")
514 .and_then(|v| v.as_array()).map(|arr| arr.iter().map(|e| {
515 let f = |k: &str| e.get(k).and_then(|v| v.as_f64()).unwrap_or(0.0);
516 crate::ranking::ScoreRow {
517 id: e.get("id").and_then(|v| v.as_str()).unwrap_or(""),
518 task_ppr: f("task_ppr"), global_ppr: f("global_ppr"),
519 lexical: f("lexical"), semantic: f("semantic"),
520 confidence: f("confidence"), criticality: f("criticality"),
521 change_risk: f("change_risk"), novelty: f("novelty"),
522 has_task: e.get("has_task").and_then(|v| v.as_bool()).unwrap_or(true),
523 }
524 }).collect()).unwrap_or_default();
525 let out = crate::ranking::score_entries(&rows);
526 serde_json::json!({"scores": out.iter().map(|(id, s)| serde_json::json!({"id": id, "score": s})).collect::<Vec<_>>()})
527 }
528 "ranking.edge_weight" => {
529 let predicate = input.get("predicate").and_then(|v| v.as_str()).unwrap_or("calls");
530 let confidence = input.get("confidence").and_then(|v| v.as_f64()).unwrap_or(1.0);
531 let total = input.get("total_symbols").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
532 let indeg = input.get("target_in_degree").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
533 let prov = match input.get("provenance").and_then(|v| v.as_str()).unwrap_or("EXTRACTED") {
538 "RESOLVED" => scc_core::Provenance::Resolved,
539 "OBSERVED" => scc_core::Provenance::Observed,
540 "DECLARED" => scc_core::Provenance::Declared,
541 "INFERRED" => scc_core::Provenance::Inferred,
542 "STALE" => scc_core::Provenance::Stale,
543 _ => scc_core::Provenance::Extracted,
544 };
545 serde_json::json!({
546 "weight": scc_context::pagerank::edge_weight(predicate, prov, confidence, total, indeg),
547 "factors": {
548 "predicate": scc_context::pagerank::predicate_weight(predicate, prov),
549 "provenance": scc_context::pagerank::provenance_weight(prov),
550 "confidence": confidence.clamp(0.0, 1.0),
551 "rarity": scc_context::pagerank::rarity(total, indeg),
552 },
553 })
554 }
555 "ranking.architectural_specificity" => {
556 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
557 let exported = input.get("exported").and_then(|v| v.as_bool()).unwrap_or(false);
558 serde_json::json!({"specificity": if exported { 1.15 } else { 1.0 }, "id": id})
559 }
560 "ranking.entry" => {
561 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
562 match ctx.surface_entry(id)? {
563 Some(e) => serde_json::to_value(&e)?,
564 None => serde_json::json!({"error": format!("entry {id} not in surface map")}),
565 }
566 }
567 "ranking.trace" => {
568 let req: scc_api::RankRequest = serde_json::from_value(input)?;
572 let goal = req.goal.clone().unwrap_or_default();
573 let mut ap = crate::plugins::active(root, &config);
574 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
575 let hooks = ranking_hooks_from_plugins(&mut ap, &goal);
576 let (out, seeds, required) = engine.ranking().trace_with_hooks(&req, &hooks)?;
577 let mut v = serde_json::to_value(&out)?;
578 v["seeds"] = serde_json::json!(seeds);
579 v["required"] = serde_json::json!(required);
580 v
581 }
582 "ranking.explain" | "surface.explain" => {
583 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
584 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
585 let req = scc_api::RankRequest { profile: None, goal: Some(goal.into()), limit: 1000, explain: true, include_features: true, include_intermediate: true };
586 let ranker = engine.ranking();
587 let mut ap = crate::plugins::active(root, &config);
588 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
589 let hooks = ranking_hooks_from_plugins(&mut ap, goal);
590 let out = ranker.symbols_with_hooks(&req, &hooks)?;
591 match out.items.into_iter().find(|i| i.id == id) {
592 Some(item) => serde_json::to_value(&item)?,
593 None => serde_json::json!({"error": format!("symbol {id} not in ranking")}),
594 }
595 }
596 "ranking.global" | "ranking.task" | "ranking.entities" => {
597 let goal = input.get("goal").and_then(|v| v.as_str()).map(|s| s.to_string());
598 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
599 let req = scc_api::RankRequest { profile: None, goal, limit, explain: false, include_features: true, include_intermediate: false };
600 let mut ap = crate::plugins::active(root, &config);
601 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
602 let hooks = ranking_hooks_from_plugins(&mut ap, req.goal.as_deref().unwrap_or(""));
603 serde_json::to_value(engine.ranking().symbols_with_hooks(&req, &hooks)?)?
604 }
605 "selection.mmr" => {
606 let req: scc_api::SelectionRequest = serde_json::from_value(input)?;
607 let ranked: Vec<(String, f64)> = req.ranked.iter().map(|e| (e.id.clone(), e.value)).collect();
608 let groups: Vec<Option<String>> = req.ranked.iter().map(|e| e.group.clone()).collect();
609 let mut ap = crate::plugins::active(root, &config);
613 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
614 if let Some(sel) = crate::plugins::diversity_selection(&ap, &req)? {
617 let mut out = serde_json::json!({"selected": sel});
618 if !ap.diagnostics.is_empty() { out["plugin_diagnostics"] = serde_json::to_value(&ap.diagnostics)?; }
619 return Ok(out);
620 }
621 let hooks = ranking_hooks_from_plugins(&mut ap, "");
622 let sims = std::sync::Arc::new(hooks.similarities);
623 let out = scc_context::selector::mmr_diversify(
624 &ranked,
625 |a: &str, b: &str| {
626 let (ga, gb) = (crate::ranking::group_of2(&ranked, &groups, a), crate::ranking::group_of2(&ranked, &groups, b));
627 crate::ranking::fold_similarity(&sims, a, b, ga, gb)
628 },
629 req.lambda.unwrap_or(0.5),
630 ranked.len(),
631 );
632 serde_json::json!({"selected": out})
633 }
634 "selection.quotas" => {
635 let req: scc_api::SelectionRequest = serde_json::from_value(input)?;
636 let rows: Vec<(String, String, f64, usize)> = req.ranked.iter().map(|e| (e.id.clone(), e.kind.clone(), e.value, e.token_cost)).collect();
637 let mut quotas: Vec<(String, f64)> = req.quotas.clone().unwrap_or_default().iter().map(|q| (q.kind.clone(), q.fraction)).collect();
638 let mut ap = crate::plugins::active(root, &config);
642 let (overrides, notes) = crate::plugins::quota_overrides(&ap, &req);
643 for (kind, frac) in &overrides {
644 if let Some(slot) = quotas.iter_mut().find(|(k, _)| k == kind) { slot.1 = *frac; }
645 else { quotas.push((kind.clone(), *frac)); }
646 }
647 if !notes.is_empty() { ap.diagnostics.extend(notes); }
648 let budget: usize = req.ranked.iter().map(|e| e.token_cost).sum();
649 let mut out = serde_json::json!({"selected": crate::ranking::apply_quotas(&rows, "as, budget)});
650 if !overrides.is_empty() {
651 out["quota_overrides"] = serde_json::json!(overrides.iter().map(|(k, f)| serde_json::json!({"kind": k, "fraction": f})).collect::<Vec<_>>());
652 }
653 if !ap.diagnostics.is_empty() { out["plugin_diagnostics"] = serde_json::to_value(&ap.diagnostics)?; }
654 out
655 }
656 "selection.required" => {
657 let goal = input.get("goal").and_then(|v| v.as_str()).unwrap_or("");
661 let mut ap = crate::plugins::active(root, &config);
662 crate::plugins::order_extensions(&crate::plugins::collect_extensions(&ap))?;
663 let hooks = ranking_hooks_from_plugins(&mut ap, goal);
664 let base: usize = engine.ranking().required_with("", &crate::ranking::RankHooks::default())?.len();
665 let mut ids: Vec<String> = engine.ranking().required_with(goal, &hooks)?.into_iter().collect();
666 ids.sort();
667 let contributed = ids.len().saturating_sub(base);
668 serde_json::json!({"required": ids, "plugin_contributed": contributed})
669 }
670 "selection.preview" => {
671 let req: scc_api::SelectionRequest = serde_json::from_value(input)?;
676 let ranked: Vec<(String, f64)> = req.ranked.iter().map(|e| (e.id.clone(), e.value)).collect();
677 let groups: Vec<Option<String>> = req.ranked.iter().map(|e| e.group.clone()).collect();
678 let sim = |a: &str, b: &str| {
679 let (ga, gb) = (crate::ranking::group_of2(&ranked, &groups, a), crate::ranking::group_of2(&ranked, &groups, b));
680 crate::ranking::default_similarity(ga, gb)
681 };
682 let after_mmr = scc_context::selector::mmr_diversify(
683 &ranked, sim, req.lambda.unwrap_or(0.5), ranked.len());
684 let rows: Vec<(String, String, f64, usize)> = req.ranked.iter()
685 .map(|e| (e.id.clone(), e.kind.clone(), e.value, e.token_cost)).collect();
686 let quotas: Vec<(String, f64)> = req.quotas.unwrap_or_default().iter()
687 .map(|q| (q.kind.clone(), q.fraction)).collect();
688 let budget: usize = req.ranked.iter().map(|e| e.token_cost).sum();
689 let after_quotas = crate::ranking::apply_quotas(&rows, "as, budget);
690 let qset: std::collections::BTreeSet<&str> =
691 after_quotas.iter().map(|x| x.as_str()).collect();
692 let items: Vec<scc_core::ContextItem> = req.ranked.iter()
693 .filter(|e| qset.contains(e.id.as_str()))
694 .map(|e| scc_core::ContextItem { id: e.id.clone(), value: e.value,
695 token_cost: e.token_cost, required: false, group: e.group.clone() }).collect();
696 let keep = crate::ranking::select_with_budget(&items, budget, budget);
697 let after_budget: Vec<String> = keep.into_iter().map(|i| items[i].id.clone()).collect();
698 serde_json::json!({
699 "after_mmr": after_mmr, "after_quotas": after_quotas,
700 "after_budget": after_budget,
701 })
702 }
703 "selection.budget" | "selection.optimize" | "surface.select" => {
704 let req: scc_api::SelectionRequest = serde_json::from_value(input)?;
705 let items: Vec<scc_core::ContextItem> = req.ranked.iter().map(|e| scc_core::ContextItem { id: e.id.clone(), value: e.value, token_cost: e.token_cost, required: false, group: e.group.clone() }).collect();
706 let budget: usize = items.iter().map(|i| i.token_cost).sum();
707 let ap = crate::plugins::active(root, &config);
710 match crate::plugins::budget_selection(&ap, &req, &items, budget)? {
711 Some(sel) => {
712 let mut out = serde_json::json!({"selected": sel});
713 if !ap.diagnostics.is_empty() { out["plugin_diagnostics"] = serde_json::to_value(&ap.diagnostics)?; }
714 out
715 }
716 None => {
717 let keep = crate::ranking::select_with_budget(&items, budget, budget);
718 let ids: Vec<&str> = keep.into_iter().map(|i| items[i].id.as_str()).collect();
719 serde_json::json!({"selected": ids})
720 }
721 }
722 }
723 "plugins.list" => {
724 let ap = crate::plugins::active(root, &config);
725 serde_json::json!({"plugins": scc_plugin_host::discover(root).iter().map(|p| &p.manifest.id).collect::<Vec<_>>(), "lock": crate::plugins::lock_entries(&ap)})
726 }
727 "plugins.describe" | "plugins.inspect" => {
728 let ap = crate::plugins::active(root, &config);
729 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
730 match ap.plugins.iter().find(|p| p.manifest.id == id) {
731 Some(p) => serde_json::json!({"manifest": {"id": p.manifest.id, "name": p.manifest.name, "version": p.manifest.version, "api": p.manifest.api, "operations": p.manifest.operations, "timeout_ms": p.manifest.timeout_ms, "failure_policy": p.manifest.failure_policy, "deterministic": p.manifest.deterministic, "runtime": format!("{:?}", p.manifest.runtime).to_lowercase(), "extensions": p.manifest.extensions}, "lock": scc_plugin_host::lock_entry(p)}),
732 None => serde_json::json!({"error": format!("unknown plugin '{id}'")}),
733 }
734 }
735 "plugins.doctor" => {
736 let ap = crate::plugins::active(root, &config);
737 serde_json::json!({"plugins": ap.plugins.iter().map(|p| serde_json::json!({"id": p.manifest.id, "operations": p.manifest.operations})).collect::<Vec<_>>(), "diagnostics": ap.diagnostics})
738 }
739 "plugins.contribute" => {
740 let plugin = input.get("plugin").and_then(|v| v.as_str()).unwrap_or("");
741 let batch = input.get("batch").cloned().unwrap_or(serde_json::json!({}));
742 if plugin.is_empty() {
743 return Err(crate::EngineError::Other("plugins.contribute requires a `plugin` id".into()));
744 }
745 crate::plugins::commit_contribution(&store, plugin, &batch)?
746 }
747 "plugins.promote" => {
748 let plugin = input.get("plugin").and_then(|v| v.as_str()).unwrap_or("");
752 if plugin.is_empty() {
753 return Err(crate::EngineError::Other("plugins.promote requires a `plugin` id".into()));
754 }
755 let assertions = input.get("assertions").cloned().unwrap_or(serde_json::json!([]));
756 crate::plugins::promote_sidecar(&store, plugin, &assertions)?
757 }
758 "plugins.lock" => {
759 let ap = crate::plugins::active(root, &config);
760 let path = scc_plugin_host::write_lockfile(root, &ap.plugins).map_err(crate::EngineError::Other)?;
761 serde_json::json!({"ok": true, "path": path.to_string_lossy(), "plugins": crate::plugins::lock_entries(&ap)})
762 }
763 "plugins.graph" => {
764 let ap = crate::plugins::active(root, &config);
765 let exts = crate::plugins::collect_extensions(&ap);
766 let order = crate::plugins::order_extensions(&exts)?;
767 let mut by_type: std::collections::BTreeMap<String, Vec<serde_json::Value>> = std::collections::BTreeMap::new();
770 for i in order {
771 let e = &exts[i];
772 by_type.entry(e.extension_type.clone()).or_default().push(
773 serde_json::json!({"id": e.id, "key": e.key(), "priority": e.priority}),
774 );
775 }
776 serde_json::json!({"groups": by_type})
777 }
778 "plugins.check" => {
779 let ap = crate::plugins::active(root, &config);
780 match scc_plugin_host::check_lockfile(root, &ap.plugins) {
781 Ok(()) => serde_json::json!({"ok": true}),
782 Err(drift) => serde_json::json!({"ok": false, "drift": drift}),
783 }
784 }
785 "plugins.enable" | "plugins.disable" => {
786 let id = input.get("id").and_then(|v| v.as_str()).unwrap_or("");
794 if id.is_empty() {
795 return Err(crate::EngineError::Other(format!("{operation} requires an `id`")));
796 }
797 let known: Vec<String> = scc_plugin_host::discover(root).into_iter().map(|p| p.manifest.id).collect();
798 if !known.iter().any(|k| k == id) {
799 return Err(crate::EngineError::Other(format!("unknown plugin '{id}' (discovered: {})", known.join(", "))));
800 }
801 let path = crate::workspace::config_path(root);
802 let mut doc: serde_yaml::Value = if path.exists() {
803 let text = std::fs::read_to_string(&path).map_err(|e| crate::EngineError::Other(e.to_string()))?;
804 serde_yaml::from_str(&text).map_err(|e| crate::EngineError::Other(e.to_string()))?
805 } else {
806 serde_yaml::from_str(&scc_indexer::Config::default_yaml()).map_err(|e| crate::EngineError::Other(e.to_string()))?
807 };
808 if !doc.get("plugins").is_some() {
809 if let Some(obj) = doc.as_mapping_mut() {
810 obj.insert(serde_yaml::Value::String("plugins".into()), serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
811 }
812 }
813 let mut cur: Vec<String> = doc.get("plugins").and_then(|p| p.get("enabled")).and_then(|v| serde_yaml::from_value(v.clone()).ok()).unwrap_or_default();
814 if operation == "plugins.enable" {
815 if !cur.iter().any(|x| x == id) { cur.push(id.to_string()); }
816 } else {
817 cur.retain(|x| x != id);
818 }
819 if let Some(plugins) = doc.get_mut("plugins").and_then(|p| p.as_mapping_mut()) {
820 plugins.insert(serde_yaml::Value::String("enabled".into()), serde_yaml::to_value(&cur).map_err(|e| crate::EngineError::Other(e.to_string()))?);
821 }
822 if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).map_err(|e| crate::EngineError::Other(e.to_string()))?; }
823 let text = serde_yaml::to_string(&doc).map_err(|e| crate::EngineError::Other(e.to_string()))?;
824 std::fs::write(&path, text).map_err(|e| crate::EngineError::Other(e.to_string()))?;
825 let cfg = crate::workspace::load_config(root)?;
826 let ap = crate::plugins::active(root, &cfg);
827 serde_json::json!({"ok": true, "enabled": cfg.plugins.enabled, "active": ap.plugins.iter().map(|p| &p.manifest.id).collect::<Vec<_>>()})
828 }
829 "plugin_state.get" | "plugin_state.put" | "plugin_state.delete" | "plugin_state.scan" => {
830 let pid = input.get("plugin").and_then(|v| v.as_str()).unwrap_or("");
831 let ap = crate::plugins::active(root, &config);
832 let plug = ap.plugins.iter().find(|p| p.manifest.id == pid).ok_or_else(|| {
833 crate::EngineError::Other(format!("unknown plugin '{pid}' (not active)"))
834 })?;
835 match operation {
836 "plugin_state.get" => {
837 let key = input.get("key").and_then(|v| v.as_str()).unwrap_or("");
838 crate::state::plugin_state_get(&store, pid, &plug.grants, key)?
839 }
840 "plugin_state.put" => {
841 let key = input.get("key").and_then(|v| v.as_str()).unwrap_or("");
842 let value = input.get("value").map(|v| v.to_string()).unwrap_or_default();
843 crate::state::plugin_state_put(&store, pid, &plug.grants, key, &value)?
844 }
845 "plugin_state.delete" => {
846 let key = input.get("key").and_then(|v| v.as_str()).unwrap_or("");
847 crate::state::plugin_state_delete(&store, pid, &plug.grants, key)?
848 }
849 _ => {
850 let prefix = input.get("prefix").and_then(|v| v.as_str()).unwrap_or("");
851 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
852 crate::state::plugin_state_scan(&store, pid, &plug.grants, prefix, limit)?
853 }
854 }
855 }
856 "sidecar.put" | "sidecar.get" | "sidecar.scan" => {
857 let pid = input.get("plugin").and_then(|v| v.as_str()).unwrap_or("");
861 let ap = crate::plugins::active(root, &config);
862 let plug = ap.plugins.iter().find(|p| p.manifest.id == pid).ok_or_else(|| {
863 crate::EngineError::Other(format!("unknown plugin '{pid}' (not active)"))
864 })?;
865 match operation {
866 "sidecar.put" => {
867 let graph = input.get("graph").and_then(|v| v.as_str()).unwrap_or("default");
868 let key = input.get("key").and_then(|v| v.as_str()).unwrap_or("");
869 let value = input.get("value").map(|v| v.to_string()).unwrap_or_default();
870 crate::state::sidecar_put(&store, pid, &plug.grants, graph, key, &value)?
871 }
872 "sidecar.get" => {
873 let graph = input.get("graph").and_then(|v| v.as_str()).unwrap_or("default");
874 let key = input.get("key").and_then(|v| v.as_str()).unwrap_or("");
875 crate::state::sidecar_get(&store, pid, &plug.grants, graph, key)?
876 }
877 _ => {
878 let graph = input.get("graph").and_then(|v| v.as_str()).unwrap_or("default");
879 let prefix = input.get("prefix").and_then(|v| v.as_str()).unwrap_or("");
880 let limit = input.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
881 crate::state::sidecar_scan(&store, pid, &plug.grants, graph, prefix, limit)?
882 }
883 }
884 }
885 "plugins.invoke" => {
886 let op = input.get("operation").and_then(|v| v.as_str()).unwrap_or(operation);
887 let inner = input.get("input").cloned().unwrap_or(serde_json::json!({}));
888 let mut ap = crate::plugins::active(root, &config);
889 crate::plugins::call_operation(&mut ap, op, inner)?
890 }
891 _ => {
892 let mut ap = crate::plugins::active(root, &config);
894 match scc_plugin_host::provider_for(&ap.plugins, operation) {
895 Ok(_) => crate::plugins::call_operation(&mut ap, operation, input)?,
896 Err(_) => return Err(crate::EngineError::Other(format!("unknown operation '{operation}' (see operations.list)"))),
897 }
898 }
899 };
900 Ok(out)
901}
902
903fn ranking_hooks_from_plugins(
905 ap: &mut crate::plugins::ActivePlugins,
906 goal: &str,
907) -> crate::ranking::RankHooks {
908 use std::sync::Arc;
909 let mut hooks = crate::ranking::RankHooks::default();
910 struct PlugSpec { id: String, ops: Vec<String>, exts: Vec<(String, String)> }
915 let specs: Vec<PlugSpec> = ap.plugins.iter()
916 .map(|p| PlugSpec { id: p.manifest.id.clone(), ops: p.manifest.operations.clone(),
917 exts: p.manifest.extensions.iter().map(|e| (e.extension_type.clone(), e.id.clone())).collect() })
918 .collect();
919 for spec in specs {
920 let plug = match ap.plugins.iter().find(|p| p.manifest.id == spec.id).cloned() {
921 Some(p) => Arc::new(p),
922 None => continue,
923 };
924 let pid = spec.id.clone();
925 let wants = |t: &str, op: &str| {
926 spec.exts.iter().any(|(ty, _)| ty == t) || spec.ops.iter().any(|o| o == op)
927 };
928 if wants("candidate-provider", "ranking.candidates") {
929 let plug = Arc::clone(&plug);
930 let pid2 = pid.clone();
931 hooks.candidates.push(Box::new(move |goal: &str| {
932 let input = serde_json::json!({"goal": goal});
933 match scc_plugin_host::call(&plug, "ranking.candidates", input, None) {
934 Ok(v) => v.get("candidates").and_then(|s| s.as_array()).map(|a| {
935 a.iter().filter_map(|e| {
936 let id = e.get("id").and_then(|x| x.as_str()).unwrap_or("").to_string();
937 if id.is_empty() { return None; }
938 Some(scc_context::rank::ScoredEntity {
939 id,
940 kind: e.get("kind").and_then(|x| x.as_str()).unwrap_or("symbol").into(),
941 name: e.get("name").and_then(|x| x.as_str()).unwrap_or("").into(),
942 score: e.get("score").and_then(|x| x.as_f64()).unwrap_or(0.0),
943 reason: format!("plugin:{}", pid2),
944 })
945 }).collect()
946 }).unwrap_or_default(),
947 Err(_) => Vec::new(),
948 }
949 }));
950 }
951 if wants("seed-provider", "ranking.seed") {
952 let plug = Arc::clone(&plug);
953 let g = goal.to_string();
954 hooks.seed_providers.push(Box::new(move |_goal| {
955 let input = serde_json::json!({"goal": g});
956 match scc_plugin_host::call(&plug, "ranking.seed", input, None) {
957 Ok(v) => v.get("seeds").and_then(|s| s.as_array()).map(|a| {
958 a.iter().map(|e| scc_core::TaskSeed {
959 kind: e.get("kind").and_then(|x| x.as_str()).unwrap_or("symbol").into(),
960 id: e.get("id").and_then(|x| x.as_str()).unwrap_or("").into(),
961 weight: e.get("weight").and_then(|x| x.as_f64()).unwrap_or(0.0),
962 }).filter(|s| !s.id.is_empty()).collect()
963 }).unwrap_or_default(),
964 Err(_) => Vec::new(),
965 }
966 }));
967 }
968 if wants("rank-feature", "ranking.feature") {
969 let plug = Arc::clone(&plug);
970 hooks.features.push(Box::new(move |sym, goal| {
971 let input = serde_json::json!({"symbol": sym, "goal": goal});
972 match scc_plugin_host::call(&plug, "ranking.feature", input, None) {
973 Ok(v) => crate::ranking::RankFeatureValue {
974 name: format!("{pid}.feature"),
975 score: v.get("score").and_then(|x| x.as_f64()).unwrap_or(0.0),
976 weight: v.get("weight").and_then(|x| x.as_f64()).unwrap_or(0.0),
977 reason: v.get("reason").and_then(|x| x.as_str()).unwrap_or("").into(),
978 },
979 Err(_) => crate::ranking::RankFeatureValue { name: format!("{pid}.feature"), score: 0.0, weight: 0.0, reason: String::new() },
980 }
981 }));
982 }
983 if wants("rank-node", "ranking.rank_nodes") {
984 let plug = Arc::clone(&plug);
989 let g = goal.to_string();
990 hooks.rank_nodes.push(Box::new(move |_goal| {
991 let input = serde_json::json!({"goal": g});
992 match scc_plugin_host::call(&plug, "ranking.rank_nodes", input, None) {
993 Ok(v) => v.get("nodes").and_then(|s| s.as_array()).map(|a| {
994 a.iter().filter_map(|e| {
995 let id = e.get("id").and_then(|x| x.as_str()).unwrap_or("").to_string();
996 let kind = e.get("kind").and_then(|x| x.as_str()).unwrap_or("").to_string();
997 if id.is_empty() || kind.is_empty() { return None; }
998 Some((id, kind))
999 }).collect()
1000 }).unwrap_or_default(),
1001 Err(_) => Vec::new(),
1002 }
1003 }));
1004 }
1005 if wants("rank-edge", "ranking.rank_edges") {
1006 let plug = Arc::clone(&plug);
1011 let g = goal.to_string();
1012 hooks.rank_edges.push(Box::new(move |_goal| {
1013 let input = serde_json::json!({"goal": g});
1014 match scc_plugin_host::call(&plug, "ranking.rank_edges", input, None) {
1015 Ok(v) => v.get("edges").and_then(|s| s.as_array()).map(|a| {
1016 a.iter().filter_map(|e| {
1017 let (sub, pred, obj) = (
1018 e.get("subject").and_then(|x| x.as_str()).unwrap_or("").to_string(),
1019 e.get("predicate").and_then(|x| x.as_str()).unwrap_or("calls").to_string(),
1020 e.get("object").and_then(|x| x.as_str()).unwrap_or("").to_string(),
1021 );
1022 let w = e.get("weight").and_then(|x| x.as_f64()).unwrap_or(0.0);
1023 if sub.is_empty() || obj.is_empty() { return None; }
1024 Some((sub, pred, obj, w))
1025 }).collect()
1026 }).unwrap_or_default(),
1027 Err(_) => Vec::new(),
1028 }
1029 }));
1030 }
1031 if wants("criticality-provider", "ranking.criticality") {
1032 let plug = Arc::clone(&plug);
1036 hooks.criticality.push(Box::new(move |sym: &str, goal: &str| {
1037 let input = serde_json::json!({"symbol": sym, "goal": goal});
1038 match scc_plugin_host::call(&plug, "ranking.criticality", input, None) {
1039 Ok(v) => v.get("criticality").and_then(|x| x.as_f64()),
1040 Err(_) => None,
1041 }
1042 }));
1043 }
1044 if wants("novelty-provider", "ranking.novelty") {
1045 let plug = Arc::clone(&plug);
1047 hooks.novelty.push(Box::new(move |sym: &str, goal: &str| {
1048 let input = serde_json::json!({"symbol": sym, "goal": goal});
1049 match scc_plugin_host::call(&plug, "ranking.novelty", input, None) {
1050 Ok(v) => v.get("novelty").and_then(|x| x.as_f64()),
1051 Err(_) => None,
1052 }
1053 }));
1054 }
1055 if wants("risk-provider", "ranking.risk") {
1056 let plug = Arc::clone(&plug);
1058 hooks.risk.push(Box::new(move |sym: &str, goal: &str| {
1059 let input = serde_json::json!({"symbol": sym, "goal": goal});
1060 match scc_plugin_host::call(&plug, "ranking.risk", input, None) {
1061 Ok(v) => v.get("risk").and_then(|x| x.as_f64()),
1062 Err(_) => None,
1063 }
1064 }));
1065 }
1066 if wants("semantic-provider", "ranking.semantic") {
1067 let plug = Arc::clone(&plug);
1069 hooks.semantic.push(Box::new(move |sym: &str, goal: &str| {
1070 let input = serde_json::json!({"symbol": sym, "goal": goal});
1071 match scc_plugin_host::call(&plug, "ranking.semantic", input, None) {
1072 Ok(v) => v.get("semantic").and_then(|x| x.as_f64()),
1073 Err(_) => None,
1074 }
1075 }));
1076 }
1077 if wants("edge-weight", "ranking.edge_weight") {
1078 let plug = Arc::clone(&plug);
1079 hooks.edge_weights.push(std::sync::Arc::new(move |subject, predicate, object, base| {
1080 let input = serde_json::json!({
1081 "subject": subject, "predicate": predicate,
1082 "object": object, "base": base,
1083 });
1084 match scc_plugin_host::call(&plug, "ranking.edge_weight", input, None) {
1085 Ok(v) => {
1086 let mode = v.get("mode").and_then(|x| x.as_str()).unwrap_or("").to_string();
1087 let value = v.get("value").and_then(|x| x.as_f64()).unwrap_or(0.0);
1088 match mode.as_str() {
1089 "add" | "multiply" | "replace" | "veto" => Some((mode, value)),
1090 _ => None,
1091 }
1092 }
1093 Err(_) => None,
1094 }
1095 }));
1096 }
1097 for eid in spec.exts.iter().filter(|(ty, _)| ty == "blend-profile").map(|(_, id)| id) {
1101 let name = eid.strip_prefix("blend-profile:").unwrap_or(eid).to_string();
1102 let plug = Arc::clone(&plug);
1103 if let Ok(v) = scc_plugin_host::call(&plug, "ranking.profile", serde_json::json!({"profile": name}), None) {
1104 let w = v.get("weights").cloned().unwrap_or(serde_json::Value::Null);
1105 let mut bw = crate::ranking::BlendWeights::default();
1106 let mut bad: Vec<String> = Vec::new();
1107 if let Some(obj) = w.as_object() {
1108 for (k, val) in obj {
1109 let num = val.as_f64();
1110 match (k.as_str(), num) {
1111 ("task_ppr", Some(x)) => bw.task_ppr = Some(x),
1112 ("global_ppr", Some(x)) => bw.global_ppr = Some(x),
1113 ("lexical", Some(x)) => bw.lexical = Some(x),
1114 ("semantic", Some(x)) => bw.semantic = Some(x),
1115 ("confidence", Some(x)) => bw.confidence = Some(x),
1116 ("criticality", Some(x)) => bw.criticality = Some(x),
1117 ("change_risk", Some(x)) => bw.change_risk = Some(x),
1118 ("novelty", Some(x)) => bw.novelty = Some(x),
1119 _ => bad.push(k.clone()),
1120 }
1121 }
1122 }
1123 if !bad.is_empty() {
1126 ap.diagnostics.push(scc_plugin_host::PluginDiagnostic {
1127 plugin: spec.id.clone(), operation: "ranking.profile".into(),
1128 error: format!("unknown blend features for profile '{name}': {}", bad.join(", ")),
1129 action: "skipped".into(),
1130 });
1131 continue;
1132 }
1133 hooks.profiles.insert(name, bw);
1134 }
1135 }
1136 if wants("coverage", "ranking.coverage") {
1137 let plug = Arc::clone(&plug);
1138 hooks.coverage.push(Box::new(move |goal: &str| {
1139 let input = serde_json::json!({"goal": goal});
1140 match scc_plugin_host::call(&plug, "ranking.coverage", input, None) {
1141 Ok(v) => v.get("required").and_then(|a| a.as_array()).map(|a| {
1142 a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()
1143 }).unwrap_or_default(),
1144 Err(_) => Vec::new(),
1145 }
1146 }));
1147 }
1148 if wants("similarity", "ranking.similarity") {
1149 let plug = Arc::clone(&plug);
1150 hooks.similarities.push(std::sync::Arc::new(move |a, b, ga, gb| {
1151 let input = serde_json::json!({"a": a, "b": b, "group_a": ga, "group_b": gb});
1152 match scc_plugin_host::call(&plug, "ranking.similarity", input, None) {
1153 Ok(v) => v.get("similarity").and_then(|x| x.as_f64()).unwrap_or(0.0),
1154 Err(_) => 0.0,
1155 }
1156 }));
1157 }
1158 if wants("reranker", "ranking.rerank") {
1159 let plug = Arc::clone(&plug);
1160 hooks.rerankers.push(Box::new(move |items, goal| {
1161 let input = serde_json::json!({"goal": goal, "items": items.iter().map(|i: &scc_api::RankItem| i.id.clone()).collect::<Vec<_>>()});
1162 if let Ok(v) = scc_plugin_host::call(&plug, "ranking.rerank", input, None) {
1163 if let Some(order) = v.get("order").and_then(|o| o.as_array()) {
1164 let pos: std::collections::HashMap<&str, usize> =
1165 order.iter().enumerate().filter_map(|(i, x)| x.as_str().map(|s| (s, i))).collect();
1166 items.sort_by_key(|i| (pos.get(i.id.as_str()).copied().unwrap_or(usize::MAX), i.id.clone()));
1167 for (k, it) in items.iter_mut().enumerate() { it.position = k + 1; }
1168 }
1169 }
1170 }));
1171 }
1172 }
1173 hooks
1174}
1175
1176fn invoke_context(
1178 ctx: &crate::SccContext,
1179 store: &scc_store::Store,
1180 config: &scc_indexer::Config,
1181 operation: &str,
1182 input: Value,
1183) -> crate::Result<Value> {
1184 Ok(match operation {
1185 "context.component" => {
1186 let req: scc_api::DetailRequest = serde_json::from_value(input)?;
1187 serde_json::to_value(ctx.component(&req)?)?
1188 }
1189 "context.flow" => {
1190 let req: scc_api::DetailRequest = serde_json::from_value(input)?;
1191 serde_json::to_value(ctx.flow(&req)?)?
1192 }
1193 "context.impact" => {
1194 let req: scc_api::ImpactRequest = serde_json::from_value(input)?;
1195 serde_json::to_value(ctx.impact(&req)?)?
1196 }
1197 "context.verify" => {
1198 let unbounded = input.get("unbounded").and_then(|v| v.as_bool()).unwrap_or(false);
1199 let mut pack = ctx.verify(unbounded)?;
1200 let (sections, notes) = crate::plugins::verify_diagnostics(&store.root, config);
1204 pack.content.push_str(§ions);
1205 for n in notes {
1206 pack.content.push_str(&format!("\n(verify diagnostic skipped: {n})\n"));
1207 }
1208 serde_json::to_value(pack)?
1209 }
1210 "context.structural" | "source.structural" => {
1211 let req: scc_api::StructuralRequest = serde_json::from_value(input)?;
1212 let goal = req.task.clone().unwrap_or_default();
1213 let (scorer, _) = crate::inference::rankers(store, config, &goal);
1214 let semantic: Option<&dyn scc_context::rank::SemanticScorer> =
1215 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
1216 let text = ctx.structural(&req, &store.root, semantic)?;
1221 let units = ctx.structural_units(&req, &store.root, semantic)?;
1222 serde_json::json!({"text": text, "units": units})
1223 }
1224 "surface.compile" => {
1225 serde_json::to_value(ctx.surface_map()?)?
1226 }
1227 "surface.render" => {
1228 let req: scc_api::SurfaceRequest = serde_json::from_value(input)?;
1229 let goal = req.task.clone().unwrap_or_default();
1230 let (scorer, _) = crate::inference::rankers(store, config, &goal);
1231 let semantic: Option<&dyn scc_context::rank::SemanticScorer> =
1232 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
1233 let (result, _) = ctx.surface(&req, semantic)?;
1234 serde_json::json!({ "text": result.text })
1235 }
1236 "surface.build" | "surface.global" | "surface.task" | "ranking.important" | "surface.important" => {
1237 let req: scc_api::SurfaceRequest = serde_json::from_value(input)?;
1238 let goal = req.task.clone().unwrap_or_default();
1239 let (scorer, _) = crate::inference::rankers(store, config, &goal);
1240 let semantic: Option<&dyn scc_context::rank::SemanticScorer> =
1241 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
1242 let (result, text) = ctx.surface(&req, semantic)?;
1243 serde_json::json!({ "result": result, "text": text })
1244 }
1245 _ => unreachable!("invoke routes only context ops here"),
1246 })
1247}
1248
1249fn status_value(store: &scc_store::Store) -> crate::Result<Value> {
1251 let repo = store.repository();
1252 let stale = crate::workspace::stale_paths(store)?;
1253 let (revision, indexed_at, branch) = match store.snapshot_status()? {
1254 Some((snap, _)) => (snap.revision, Some(snap.indexed_at), snap.branch),
1255 None => ("not-indexed".to_string(), None, None),
1256 };
1257 Ok(serde_json::json!({
1258 "repository": repo.name,
1259 "repository_id": repo.id,
1260 "remote": repo.url,
1261 "revision": revision,
1262 "branch": branch,
1263 "indexed_at": indexed_at,
1264 "stats": store.stats()?,
1265 "freshness": if stale.is_empty() { "CURRENT" } else { "STALE" },
1266 "stale_files": stale.iter().take(10).collect::<Vec<_>>(),
1267 "stale_count": stale.len(),
1268 }))
1269}
1270
1271fn plugin_export(
1276 root: &std::path::Path,
1277 config: &scc_indexer::Config,
1278 format: &str,
1279) -> Option<Value> {
1280 let ap = crate::plugins::active(root, config);
1284 let specs: Vec<(String, String, String)> = ap
1285 .plugins
1286 .iter()
1287 .flat_map(|pl| {
1288 pl.manifest.extensions.iter()
1289 .filter(|e| e.extension_type == "exporter" && e.id.strip_prefix("exporter:").unwrap_or(&e.id) == format)
1290 .map(|e| (pl.manifest.id.clone(), e.id.clone(), pl.manifest.failure_policy.clone()))
1291 })
1292 .collect();
1293 for (pid, ext_id, policy) in specs {
1294 let plug = match ap.plugins.iter().find(|pl| pl.manifest.id == pid).cloned() {
1295 Some(pl) => pl,
1296 None => continue,
1297 };
1298 match scc_plugin_host::call(&plug, "export.render", serde_json::json!({"format": format}), None) {
1299 Ok(v) => {
1300 let has = v.get("format-available").and_then(|x| x.as_bool()).unwrap_or(false);
1301 let body = v.get("text").or_else(|| v.get("json"));
1302 if has && body.is_some() {
1303 let mut out = serde_json::json!({"format": format, "plugin": pid, "extension": ext_id});
1306 out["output"] = body.cloned().unwrap();
1307 return Some(out);
1308 }
1309 }
1310 Err(e) => {
1311 if policy == "required" {
1312 return Some(serde_json::json!({"error": format!("exporter {ext_id} from {pid} FAILED: {e}")}));
1313 }
1314 }
1315 }
1316 }
1317 None
1318}
1319
1320fn export_value(store: &scc_store::Store, root: &std::path::Path, format: &str) -> crate::Result<Value> {
1322 if let Ok(config) = crate::workspace::load_config(root) {
1326 if let Some(v) = plugin_export(root, &config, format) {
1327 if v.get("error").is_some() {
1328 let msg = v["error"].as_str().unwrap_or("exporter failed");
1329 return Err(crate::EngineError::Other(msg.into()));
1330 }
1331 return Ok(v);
1332 }
1333 }
1334 let ir = crate::exports::system_ir(store)?;
1335 match format {
1336 "system-ir.json" | "system-ir.jsonl" | "ccg" | "flow-graphs.json" => {
1337 Ok(match format {
1338 "system-ir.jsonl" => Value::Array(crate::exports::jsonl(&ir)?.into_iter().map(Value::String).collect()),
1339 "ccg" => crate::exports::ccg(&ir)?,
1340 "flow-graphs.json" => serde_json::to_value(store.flow_graphs()?)?,
1341 _ => serde_json::to_value(&ir)?,
1342 })
1343 }
1344 "capsule.md" => Ok(Value::String(crate::exports::capsule(root)?)),
1345 other => Err(crate::EngineError::Other(format!(
1346 "unknown export format '{other}' (use system-ir.json, system-ir.jsonl, ccg, flow-graphs.json, capsule.md)"
1347 ))),
1348 }
1349}