1use scc_api::{DetailRequest, ImpactRequest, StartupRequest, StructuralRequest, SurfaceRequest as ApiSurfaceRequest};
8use scc_context::startup::{allocate_startup_budget, build_startup, render_startup, task_delta_with_ids, visible_ids_from_startup, StartupContext};
9use scc_core::{ContextBudget, SurfaceRenderResult};
10use std::path::Path;
11
12pub struct SccContext<'a> {
14 pub(crate) engine: &'a crate::workspace::Engine<'a>,
15}
16
17pub(crate) fn record_visible_ids(
19 led: &mut scc_core::ContextLedger,
20 ctx: &scc_context::ContextCompiler,
21 ids: &[String],
22) {
23 for id in ids {
24 led.visible_entities.insert(id.clone());
25 if let Some(e) = ctx.view.entity(id) {
26 match e.kind.as_str() {
27 scc_core::kinds::SYMBOL => {
28 led.visible_symbols.insert(id.clone());
29 }
30 scc_core::kinds::COMPONENT => {
31 led.visible_components.insert(id.clone());
32 }
33 scc_core::kinds::FLOW => {
34 led.visible_flows.insert(id.clone());
35 }
36 _ => {}
37 }
38 }
39 }
40}
41
42fn record_rendered_entries(
44 led: &mut scc_core::ContextLedger,
45 entries: &[scc_core::SurfaceEntry],
46 rendered_ids: &[String],
47) {
48 if !entries.is_empty() {
49 for e in entries {
50 led.visible_entities.insert(e.symbol_id.clone());
51 led.visible_symbols.insert(e.symbol_id.clone());
52 led.visible_files.insert(e.path.clone());
53 if let Some(c) = &e.component {
54 led.visible_components.insert(c.clone());
55 }
56 }
57 return;
58 }
59 for id in rendered_ids {
60 led.visible_entities.insert(id.clone());
61 }
62}
63
64impl SccContext<'_> {
66 pub fn overview(&self) -> crate::Result<scc_context::ContextPack> {
68 Ok(self.engine.ctx().system_overview())
69 }
70
71 pub fn atlas(&self, budget: Option<usize>, full: bool, unbounded: bool) -> crate::Result<scc_context::ContextPack> {
73 let ctx = self.engine.ctx();
74 Ok(if full {
75 ctx.system_atlas_scoped(budget, scc_context::atlas::AtlasScope::Full, unbounded)
76 } else if unbounded {
77 ctx.system_atlas_scoped(budget, scc_context::atlas::AtlasScope::Production, true)
78 } else {
79 ctx.system_atlas(budget)
80 })
81 }
82
83 pub fn atlas_model(
87 &self,
88 scope: scc_context::atlas::AtlasScope,
89 ) -> crate::Result<scc_core::SystemAtlas> {
90 let ctx = self.engine.ctx();
91 Ok(scc_context::atlas::build_atlas_scoped(&ctx, scope))
92 }
93
94 pub fn startup_model(
99 &self,
100 req: &StartupRequest,
101 ) -> crate::Result<crate::startup_model::StartupModel> {
102 let (startup, _text) = self.startup(req)?;
103 Ok(crate::startup_model::StartupModel::from_context(startup))
104 }
105
106 pub fn startup(&self, req: &StartupRequest) -> crate::Result<(StartupContext, String)> {
108 let ctx = self.engine.ctx();
109 let budget = allocate_startup_budget(&ctx, req.budget);
110 let startup = build_startup(&ctx, &budget, scc_context::startup::RENDERER_VERSION);
111 let text = render_startup(&startup);
112 let ledger_store = scc_context::context_ledger::ContextLedgerStore::new(self.engine.store);
113 let mut led = ledger_store.load();
114 let (syms, files, comps, flows) = visible_ids_from_startup(&ctx, &startup);
115 led.visible_entities.extend(syms.iter().cloned());
116 led.visible_symbols.extend(syms);
117 led.visible_files.extend(files);
118 led.visible_components.extend(comps);
119 led.visible_flows.extend(flows);
120 ledger_store.save(&led);
121 Ok((startup, text))
122 }
123
124 pub fn surface_map(&self) -> crate::Result<scc_core::SystemSurfaceMap> {
126 let ctx = self.engine.ctx();
127 Ok(scc_context::surface::compile_surface_map(&ctx))
128 }
129
130 pub fn surface_entry(&self, id: &str) -> crate::Result<Option<scc_core::SurfaceEntry>> {
132 let ctx = self.engine.ctx();
133 let map = scc_context::surface::compile_surface_map(&ctx);
134 Ok(map.entries.into_iter().find(|e| e.id == id))
135 }
136
137 pub fn surface(&self, req: &ApiSurfaceRequest, semantic: Option<&dyn scc_context::rank::SemanticScorer>) -> crate::Result<(SurfaceRenderResult, String)> {
139 let ctx = self.engine.ctx();
140 let tokens = req.budget.unwrap_or(ContextBudget::default().surface);
141 let request = scc_context::surface::SurfaceRequest {
142 mode: match req.task.as_deref() {
143 Some(goal) => scc_context::surface::SurfaceMode::Task { goal, visible: None },
144 None => scc_context::surface::SurfaceMode::Global,
145 },
146 budget: tokens,
147 explain: req.explain,
148 policy: scc_context::surface::SurfacePolicy::defaults(tokens),
149 semantic,
150 };
151 let stages = req.stages.as_ref().map(|st| scc_context::surface::SurfacePipelineStages {
152 lexical: st.lexical, global_ppr: st.global_ppr, task_ppr: st.task_ppr,
153 mmr: st.mmr, quotas: st.quotas, optimizer: st.optimizer,
154 }).unwrap_or_default();
155 let result = scc_context::surface::build_surface_staged(&ctx, request, &stages);
156 let text = match req.task.as_deref() {
157 Some(goal) => {
158 let body = result.text.strip_prefix("SCC SYSTEM SURFACE MAP").unwrap_or(result.text.as_str());
159 format!("# SYSTEM SURFACE MAP (task-personalized: {goal}){body}")
160 }
161 None => result.text.clone(),
162 };
163 if !result.rendered_ids.is_empty() {
164 let store = self.engine.store;
165 let mut led = scc_context::context_ledger::ContextLedgerStore::new(store).load();
166 record_rendered_entries(&mut led, &result.rendered_entries, &result.rendered_ids);
167 scc_context::context_ledger::ContextLedgerStore::new(store).save(&led);
168 }
169 Ok((result, text))
170 }
171
172 pub fn important(&self, limit: usize, component: Option<&str>, task: Option<&str>) -> crate::Result<(Vec<scc_core::SurfaceEntry>, bool)> {
174 use scc_context::surface::{important_symbols, SurfaceMode};
175 let ctx = self.engine.ctx();
176 let mode = match task {
177 Some(goal) => SurfaceMode::Task { goal, visible: None },
178 None => SurfaceMode::Global,
179 };
180 let tasked = task.is_some();
181 let mut entries = important_symbols(&ctx, mode, 0);
182 if let Some(c) = component {
183 entries.retain(|e| e.component.as_deref().is_some_and(|s| s.contains(c)));
184 }
185 entries.truncate(limit.max(1));
186 Ok((entries, tasked))
187 }
188
189 pub fn subagent(&self, goal: &str, files: &[String], symbols: &[String], budget: Option<usize>) -> crate::Result<scc_context::ContextPack> {
191 let mut pack = self.engine.ctx().task_context(goal, files, symbols, budget);
192 pack.kind = "subagent".into();
193 let mut header = String::new();
194 header.push_str("# SUBAGENT SCOPE\n");
195 header.push_str("You are a delegated agent. Work ONLY within the context below; ");
196 header.push_str("do not re-derive the system model. If a needed fact is absent, ");
197 header.push_str("state it and ask rather than assume. Your goal is bounded to:\n");
198 header.push_str(&format!("> {goal}\n\n"));
199 pack.content = format!("{header}{}", pack.content);
200 Ok(pack)
201 }
202
203 pub fn component(&self, req: &DetailRequest) -> crate::Result<scc_context::ContextPack> {
205 let ctx = self.engine.ctx();
206 Ok(if req.unbounded { ctx.component_context_full(&req.id) } else { ctx.component_context(&req.id) })
207 }
208
209 pub fn flow(&self, req: &DetailRequest) -> crate::Result<scc_context::ContextPack> {
211 let ctx = self.engine.ctx();
212 Ok(if req.unbounded { ctx.flow_context_full(&req.id) } else { ctx.flow_context(&req.id) })
213 }
214
215 pub fn impact(&self, req: &ImpactRequest) -> crate::Result<scc_context::ContextPack> {
217 let ctx = self.engine.ctx();
218 Ok(if req.unbounded {
219 ctx.impact_context_full(&req.files, &req.symbols, req.diff.as_deref())
220 } else {
221 ctx.impact_context(&req.files, &req.symbols, req.diff.as_deref())
222 })
223 }
224
225 pub fn verify(&self, unbounded: bool) -> crate::Result<scc_context::ContextPack> {
227 let ctx = self.engine.ctx();
228 Ok(if unbounded { ctx.verify_context_full() } else { ctx.verify_context() })
229 }
230
231 pub fn structural_units(
234 &self,
235 req: &StructuralRequest,
236 root: &Path,
237 semantic: Option<&dyn scc_context::rank::SemanticScorer>,
238 ) -> crate::Result<Vec<scc_core::StructuralSourceUnit>> {
239 let store = self.engine.store;
240 let ctx = self.engine.ctx();
241 let tokens = req.budget.unwrap_or(ContextBudget::default().structural_source);
242 let max_units = (tokens / 1000).clamp(1, 64);
243 let paths: Vec<String> = if !req.files.is_empty() {
244 let mut resolved = Vec::new();
245 for f in &req.files {
246 match scc_context::structural_source::resolve_handle_to_path(&store.root, f) {
247 Ok(p) => resolved.push(p),
248 Err(_) => return Ok(Vec::new()),
249 }
250 }
251 resolved
252 } else if let Some(goal) = req.task.as_deref() {
253 let goal = goal.trim();
254 if goal.is_empty() {
255 return Ok(Vec::new());
256 }
257 super::task::surface_task_files(&ctx, goal, max_units, tokens.max(1), semantic)
258 } else {
259 return Ok(Vec::new());
260 };
261 if paths.is_empty() {
262 return Ok(Vec::new());
263 }
264 let _ = root;
265 Ok(scc_context::structural_source::structural_source(&ctx, &paths, max_units))
266 }
267
268 pub fn structural(
270 &self,
271 req: &StructuralRequest,
272 root: &Path,
273 semantic: Option<&dyn scc_context::rank::SemanticScorer>,
274 ) -> crate::Result<String> {
275 for f in &req.files {
278 if let Err(e) = scc_context::structural_source::resolve_handle_to_path(&self.engine.store.root, f) {
279 return Ok(format!("# HANDLE REFUSED\n{e}\n"));
280 }
281 }
282 if req.files.is_empty() && req.task.as_deref().map(|g| g.trim().is_empty()).unwrap_or(true) {
283 return Ok("# STRUCTURAL SOURCE\n\nPass --files <paths...> or --task \"<goal>\".".to_string());
284 }
285 let units = self.structural_units(req, root, semantic)?;
286 if units.is_empty() {
287 return Ok("# STRUCTURAL SOURCE\n\nNo indexed files matched (run `scc index` first, or pass --files explicitly).".to_string());
288 }
289 Ok(scc_context::structural_source::render_structural(&units))
290 }
291
292 pub fn task_delta(&self, goal: &str, budget: usize, semantic: Option<&dyn scc_context::rank::SemanticScorer>) -> crate::Result<(String, Vec<String>)> {
294 let ctx = self.engine.ctx();
295 let ledger_store = scc_context::context_ledger::ContextLedgerStore::new(self.engine.store);
296 let visible = ledger_store.load();
297 Ok(task_delta_with_ids(&ctx, goal, &visible, budget, semantic))
298 }
299
300 pub fn record_task_delta_ids(&self, ids: &[String]) {
302 if ids.is_empty() {
303 return;
304 }
305 let ctx = self.engine.ctx();
306 let ledger_store = scc_context::context_ledger::ContextLedgerStore::new(self.engine.store);
307 let mut led = ledger_store.load();
308 record_visible_ids(&mut led, &ctx, ids);
309 ledger_store.save(&led);
310 }
311}