1use std::path::{Path, PathBuf};
14
15pub struct SccEngine {
18 root: PathBuf,
19 config: scc_indexer::Config,
20 store: scc_store::Store,
21}
22
23impl SccEngine {
25 pub fn open(root: impl AsRef<Path>) -> crate::Result<Self> {
28 let root = root.as_ref().to_path_buf();
29 let config = crate::workspace::load_config(&root)?;
30 let store = crate::workspace::open_store(&root)?;
31 Ok(SccEngine { root, config, store })
32 }
33
34 pub fn root(&self) -> &Path {
37 &self.root
38 }
39
40 pub fn invoke(
44 &self,
45 operation: &str,
46 input: serde_json::Value,
47 ) -> crate::Result<serde_json::Value> {
48 crate::invoke::invoke(&self.root, operation, input)
49 }
50
51 pub fn session(&self) -> crate::Result<crate::workspace::Session> {
56 crate::workspace::open_session(&self.store, &self.config)
57 }
58
59 pub fn invoke_session(
63 &self,
64 session: &crate::workspace::Session,
65 operation: &str,
66 input: serde_json::Value,
67 ) -> crate::Result<serde_json::Value> {
68 self.with_engine(|engine| engine.invoke_session(session, operation, input))
69 }
70
71 pub fn refresh(&mut self) -> crate::Result<()> {
75 self.store = crate::workspace::open_store(&self.root)?;
76 self.config = crate::workspace::load_config(&self.root)?;
77 Ok(())
78 }
79
80 pub fn with_engine<T>(
85 &self,
86 f: impl FnOnce(crate::workspace::Engine<'_>) -> crate::Result<T>,
87 ) -> crate::Result<T> {
88 let engine = crate::workspace::open_engine(
89 &self.store,
90 &self.config,
91 crate::workspace::stale_paths(&self.store)?,
92 )?;
93 f(engine)
94 }
95
96 pub fn workspace_status(&self) -> crate::Result<crate::status::Status> {
98 crate::status::status(&self.store)
99 }
100
101 pub fn index_full(&self) -> crate::Result<scc_indexer::IndexReport> {
103 let report = crate::index::full(&self.root, &self.config)?;
104 self.refresh_store_only()?;
105 Ok(report)
106 }
107
108 pub fn index_paths(&self, paths: &[String]) -> crate::Result<scc_indexer::IndexReport> {
110 let report = crate::index::refresh_paths(&self.root, &self.config, paths)?;
111 self.refresh_store_only()?;
112 Ok(report)
113 }
114
115 fn refresh_store_only(&self) -> crate::Result<()> {
117 Ok(())
122 }
123
124 pub fn graph_entity(&self, id: &str) -> crate::Result<Option<scc_core::Entity>> {
126 Ok(self.store.get_entity(id)?)
127 }
128
129 pub fn graph_relationships(
131 &self,
132 subject: Option<&str>,
133 predicate: Option<&str>,
134 limit: usize,
135 ) -> crate::Result<Vec<scc_core::Relationship>> {
136 crate::graph::relationships(&self.store, subject, predicate, limit)
137 }
138
139 pub fn evidence_list(
141 &self,
142 path: Option<&str>,
143 limit: usize,
144 ) -> crate::Result<Vec<scc_core::Evidence>> {
145 let all = self.store.all_evidence()?;
146 Ok(all
147 .into_iter()
148 .filter(|e| path.is_none_or(|p| e.path.as_deref().is_some_and(|ep| ep.contains(p))))
149 .take(limit.max(1))
150 .collect())
151 }
152
153 pub fn model(&self) -> crate::Result<serde_json::Value> {
155 crate::exports::model_get(&self.store)
156 }
157
158 pub fn history(&self) -> crate::Result<Vec<scc_store::history::GraphRevision>> {
160 crate::history::revisions(&self.store)
161 }
162
163 pub fn history_diff(
165 &self,
166 from: i64,
167 to: i64,
168 ) -> crate::Result<scc_store::history::SemanticDelta> {
169 crate::history::diff(&self.store, from, to)
170 }
171
172 pub fn runtime_status(&self) -> crate::Result<Vec<scc_indexer::runtime::RuntimeEdge>> {
174 crate::state::runtime_edges(&self.root)
175 }
176
177 pub fn runtime_reconcile(&self) -> crate::Result<scc_indexer::runtime::Reconciliation> {
179 crate::state::reconcile(&self.root)
180 }
181
182 pub fn surface_build(
187 &self,
188 task: &str,
189 budget: usize,
190 explain: bool,
191 ) -> crate::Result<(scc_core::SurfaceRenderResult, String)> {
192 self.with_engine(|engine| {
193 let req = scc_api::SurfaceRequest {
194 task: Some(task.to_string()),
195 budget: Some(budget),
196 explain,
197 stages: None,
198 };
199 let (scorer, _) = crate::inference::rankers(engine.store, &self.config, task);
200 let semantic: Option<&dyn scc_context::rank::SemanticScorer> =
201 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
202 engine.context().surface(&req, semantic)
203 })
204 }
205
206 pub fn ranking_symbols(
212 &self,
213 goal: &str,
214 limit: usize,
215 explain: bool,
216 ) -> crate::Result<scc_api::RankResult> {
217 self.with_engine(|engine| {
218 engine.ranking().symbols(&scc_api::RankRequest {
219 goal: Some(goal.to_string()),
220 profile: None,
221 limit,
222 explain,
223 include_features: false,
224 include_intermediate: false,
225 })
226 })
227 }
228
229 pub fn context_task(
233 &self,
234 goal: &str,
235 budget: usize,
236 ) -> crate::Result<crate::task::TaskContextArtifact> {
237 self.with_engine(|engine| {
238 let req = scc_api::TaskContextRequest {
239 goal: goal.to_string(),
240 files: vec![],
241 symbols: vec![],
242 budget: Some(budget),
243 hook: false,
244 record_visibility: true,
245 };
246 let (scorer, reranker) = crate::inference::rankers(engine.store, &self.config, goal);
247 let scorer_trait: Option<&dyn scc_context::rank::SemanticScorer> =
248 scorer.as_ref().map(|s| s as &dyn scc_context::rank::SemanticScorer);
249 let reranker_trait: Option<&dyn scc_context::rank::Reranker> =
250 reranker.as_ref().map(|r| r as &dyn scc_context::rank::Reranker);
251 crate::task::build_task_context(&engine, &self.config, &self.root, &req, scorer_trait, reranker_trait)
252 })
253 }
254
255 pub fn context_atlas(&self, budget: usize) -> crate::Result<scc_context::ContextPack> {
259 self.with_engine(|engine| engine.context().atlas(Some(budget), false, false))
260 }
261
262 pub fn context_ns(&self) -> ContextNs<'_> {
266 ContextNs { engine: self }
267 }
268
269 pub fn surface_ns(&self) -> SurfaceNs<'_> {
272 SurfaceNs { engine: self }
273 }
274
275 pub fn ranking_ns(&self) -> RankingNs<'_> {
278 RankingNs { engine: self }
279 }
280
281 pub fn graph_ns(&self) -> GraphNs<'_> {
284 GraphNs { engine: self }
285 }
286}
287
288pub struct ContextNs<'a> {
291 engine: &'a SccEngine,
292}
293
294impl ContextNs<'_> {
296 pub fn overview(&self) -> crate::Result<scc_context::ContextPack> {
298 self.engine.with_engine(|e| e.context().overview())
299 }
300 pub fn atlas(&self, budget: usize) -> crate::Result<scc_context::ContextPack> {
302 self.engine.context_atlas(budget)
303 }
304 pub fn atlas_model(
306 &self,
307 scope: scc_context::atlas::AtlasScope,
308 ) -> crate::Result<scc_core::SystemAtlas> {
309 self.engine.with_engine(|e| e.context().atlas_model(scope))
310 }
311 pub fn task(
313 &self,
314 goal: &str,
315 budget: usize,
316 ) -> crate::Result<crate::task::TaskContextArtifact> {
317 self.engine.context_task(goal, budget)
318 }
319}
320
321pub struct SurfaceNs<'a> {
324 engine: &'a SccEngine,
325}
326
327impl SurfaceNs<'_> {
329 pub fn build(
331 &self,
332 task: &str,
333 budget: usize,
334 explain: bool,
335 ) -> crate::Result<(scc_core::SurfaceRenderResult, String)> {
336 self.engine.surface_build(task, budget, explain)
337 }
338}
339
340pub struct RankingNs<'a> {
343 engine: &'a SccEngine,
344}
345
346impl RankingNs<'_> {
348 pub fn symbols(
350 &self,
351 goal: &str,
352 limit: usize,
353 explain: bool,
354 ) -> crate::Result<scc_api::RankResult> {
355 self.engine.ranking_symbols(goal, limit, explain)
356 }
357}
358
359pub struct GraphNs<'a> {
362 engine: &'a SccEngine,
363}
364
365impl GraphNs<'_> {
367 pub fn entity(&self, id: &str) -> crate::Result<Option<scc_core::Entity>> {
369 self.engine.graph_entity(id)
370 }
371 pub fn relationships(
373 &self,
374 subject: Option<&str>,
375 predicate: Option<&str>,
376 limit: usize,
377 ) -> crate::Result<Vec<scc_core::Relationship>> {
378 self.engine.graph_relationships(subject, predicate, limit)
379 }
380 pub fn query(
382 &self,
383 query: &str,
384 limit: usize,
385 ) -> crate::Result<crate::graph::QueryHit> {
386 self.engine.with_engine(|_| {
387 crate::graph::query(
388 &self.engine.store,
389 &scc_api::QueryRequest { query: query.to_string(), limit },
390 )
391 })
392 }
393}