Skip to main content

scc_engine/
context.rs

1//! Engine context operations: every context pack behind one seam.
2//!
3//! Each function is the body of its `cmd_*` twin with the `println`
4//! removed — the engine returns values, transports render them.
5//! Byte-identical derivation: same builders, same order, same budgets.
6
7use 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
12// trace:v1 id=impl.crates-scc-engine-src-context.scc-context work=WORK-SI-MMMJA4G6 implements=PLAN-SI-SYKFPBEC
13pub struct SccContext<'a> {
14    pub(crate) engine: &'a crate::workspace::Engine<'a>,
15}
16
17// trace:exempt reason=internal-detail
18pub(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
42// trace:exempt reason=internal-detail
43fn 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
64// trace:v1 id=impl.crates-scc-engine-src-context.scc-context-2 work=WORK-SI-MMMJA4G6 implements=PLAN-SI-SYKFPBEC
65impl SccContext<'_> {
66    // trace:exempt reason=internal-detail
67    pub fn overview(&self) -> crate::Result<scc_context::ContextPack> {
68        Ok(self.engine.ctx().system_overview())
69    }
70
71    // trace:exempt reason=internal-detail
72    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    /// Structured SystemAtlas model (§74): the same compilation the
84    /// atlas pack renders, as its actual type — no text scraping.
85    // trace:exempt reason=internal-detail
86    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    /// Structured startup decomposition (§75): atlas model + skeleton +
95    /// surface render + coverage + omissions + artifact, each addressable
96    /// without parsing the rendered text.
97    // trace:exempt reason=internal-detail
98    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    // trace:exempt reason=internal-detail
107    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    // trace:exempt reason=internal-detail
125    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    // trace:exempt reason=internal-detail
131    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    // trace:exempt reason=internal-detail
138    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    // trace:exempt reason=internal-detail
173    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    // trace:exempt reason=internal-detail
190    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    // trace:exempt reason=internal-detail
204    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    // trace:exempt reason=internal-detail
210    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    // trace:exempt reason=internal-detail
216    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    // trace:exempt reason=internal-detail
226    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    // trace:exempt reason=internal-detail
232    // trace:exempt reason=internal-detail
233    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    // trace:exempt reason=internal-detail
269    pub fn structural(
270        &self,
271        req: &StructuralRequest,
272        root: &Path,
273        semantic: Option<&dyn scc_context::rank::SemanticScorer>,
274    ) -> crate::Result<String> {
275        // Handle-refusal and empty-input envelopes live here (transport
276        // guidance, not model data); the units path is the authority.
277        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    // trace:exempt reason=internal-detail
293    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    // trace:exempt reason=internal-detail
301    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}