1pub mod atlas;
8pub mod budget;
9pub mod context_ledger;
10pub mod packs;
11pub mod pagerank;
12pub mod rank;
13pub mod relevance;
14mod repo_path;
15pub mod selector;
16pub mod skeleton;
17pub mod startup;
18pub mod structural_source;
19pub mod surface;
20
21pub use surface::{
26 build_surface, build_surface_staged, SurfaceMode, SurfacePipelineStages, SurfacePolicy,
27 SurfaceRequest,
28};
29
30use scc_core::estimate_tokens;
31use scc_graph::{RealityGraph, TrustedGraphView, TrustPolicy};
32use scc_store::Store;
33use serde::{Deserialize, Serialize};
34use std::collections::BTreeMap;
35
36#[derive(Debug, Clone)]
37pub struct ContextSettings {
40 pub startup_tokens: usize,
41 pub task_tokens: usize,
42 pub atlas_tokens: usize,
43 pub detail_tokens: usize,
48 pub include_low_confidence_inference: bool,
49 pub rank_salt: String,
53 pub pack_allocator: PackAllocator,
57}
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
61pub enum PackAllocator {
63 #[default]
64 AdaptivePriority,
65 FixedRollover,
66}
67
68impl Default for ContextSettings {
70 fn default() -> Self {
72 ContextSettings {
73 startup_tokens: 6000,
74 task_tokens: 10000,
75 atlas_tokens: 15000,
76 detail_tokens: 6000,
77 include_low_confidence_inference: false,
78 rank_salt: String::new(),
79 pack_allocator: PackAllocator::AdaptivePriority,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
85pub struct ContextPack {
87 pub kind: String,
88 pub repository_revision: String,
89 pub content: String,
90 #[serde(default)]
91 pub entity_ids: Vec<String>,
92 #[serde(default)]
93 pub evidence_summary: BTreeMap<String, usize>,
94 #[serde(default)]
95 pub warnings: Vec<String>,
96 #[serde(default)]
97 pub tokens: usize,
98 #[serde(default)]
99 pub budget: usize,
100 #[serde(default)]
102 pub original_tokens: usize,
103 #[serde(default)]
105 pub dropped_sections: Vec<String>,
106 #[serde(default)]
108 pub hard_truncated: bool,
109 #[serde(default)]
112 pub exceeded_soft_budget: bool,
113 #[serde(default)]
116 pub truncated: bool,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub compression_policy: Option<serde_json::Value>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub analysis_quality: Option<scc_core::AnalysisQuality>,
125}
126
127impl ContextPack {
128 fn new(kind: &str, revision: &str) -> Self {
129 ContextPack {
130 kind: kind.to_string(),
131 repository_revision: revision.to_string(),
132 content: String::new(),
133 entity_ids: Vec::new(),
134 evidence_summary: BTreeMap::new(),
135 warnings: Vec::new(),
136 tokens: 0,
137 budget: 0,
138 original_tokens: 0,
139 dropped_sections: Vec::new(),
140 hard_truncated: false,
141 exceeded_soft_budget: false,
142 truncated: false,
143 compression_policy: None,
144 analysis_quality: None,
145 }
146 }
147}
148
149pub struct ContextCompiler<'a> {
151 pub store: &'a Store,
152 pub view: TrustedGraphView<'a>,
156 pub settings: ContextSettings,
157 pub stale_paths: Vec<String>,
160 evidence: std::cell::RefCell<Option<std::collections::HashMap<String, scc_core::Evidence>>>,
164}
165
166impl<'a> ContextCompiler<'a> {
168 pub fn new(
169 store: &'a Store,
170 graph: &'a RealityGraph,
171 settings: ContextSettings,
172 stale_paths: Vec<String>,
173 ) -> Self {
174 let floor = if settings.include_low_confidence_inference {
178 0.0
179 } else {
180 0.85
181 };
182 let view = TrustedGraphView::new(
183 graph,
184 store,
185 &stale_paths,
186 TrustPolicy::default().with_inferred_floor(floor),
187 );
188 ContextCompiler {
189 store,
190 view,
191 settings,
192 stale_paths,
193 evidence: std::cell::RefCell::new(None),
194 }
195 }
196
197 pub fn evidence_map(&self) -> std::cell::Ref<'_, std::collections::HashMap<String, scc_core::Evidence>> {
201 if self.evidence.borrow().is_none() {
202 *self.evidence.borrow_mut() = Some(
203 self.store
204 .all_evidence()
205 .ok()
206 .map(|evs| evs.into_iter().map(|e| (e.id.clone(), e)).collect())
207 .unwrap_or_default(),
208 );
209 }
210 std::cell::Ref::map(self.evidence.borrow(), |slot| {
211 slot.as_ref().expect("set above")
212 })
213 }
214
215 pub fn revision(&self) -> String {
216 self.store
217 .latest_snapshot()
218 .ok()
219 .flatten()
220 .map(|s| s.revision)
221 .unwrap_or_else(|| "not-indexed".to_string())
222 }
223
224 pub fn is_stale_path(&self, path: &str) -> bool {
225 self.stale_paths.iter().any(|p| p == path)
226 }
227
228 pub fn evidence_summary(&self, entity_ids: &[String]) -> BTreeMap<String, usize> {
230 let mut m: BTreeMap<String, usize> = BTreeMap::new();
231 let evmap = self.evidence_map();
232 for id in entity_ids {
233 if let Some(e) = self.view.entity(id) {
234 for ev_id in &e.evidence {
235 if let Some(ev) = evmap.get(ev_id) {
236 let path = ev.path.clone().unwrap_or_default();
237 if self.is_stale_path(&path) {
238 *m.entry("STALE".into()).or_insert(0) += 1;
239 continue;
240 }
241 let et = match ev.r#type {
242 scc_core::EvidenceType::Source => "SOURCE",
243 scc_core::EvidenceType::Config => "CONFIG",
244 scc_core::EvidenceType::Runtime => "RUNTIME",
245 scc_core::EvidenceType::Test => "TEST",
246 scc_core::EvidenceType::Intent => "INTENT",
247 scc_core::EvidenceType::History => "HISTORY",
248 };
249 *m.entry(et.to_string()).or_insert(0) += 1;
250 }
251 }
252 }
253 }
254 for r in self.view.all_rels() {
255 if entity_ids.contains(&r.subject)
256 && !r.evidence.is_empty() {
257 let key = r.provenance.as_str().to_string();
258 *m.entry(key).or_insert(0) += 1;
259 }
260 }
261 m
262 }
263
264 pub fn apply_budget(&mut self, pack: &mut ContextPack, budget: usize) {
270 pack.budget = budget;
271 if pack.original_tokens == 0 {
272 pack.original_tokens = estimate_tokens(&pack.content);
273 }
274 pack.tokens = estimate_tokens(&pack.content);
275 let over = pack.tokens > budget;
276 pack.truncated = pack.hard_truncated || !pack.dropped_sections.is_empty() || over;
277 if over && !pack.hard_truncated {
278 pack.exceeded_soft_budget = true;
279 }
280 }
281
282 pub fn system_overview(&self) -> ContextPack {
285 packs::overview(self)
286 }
287
288 pub fn system_atlas(&self, budget: Option<usize>) -> ContextPack {
294 self.system_atlas_scoped(budget, atlas::AtlasScope::Production, false)
295 }
296
297 pub fn system_atlas_scoped(
301 &self,
302 budget: Option<usize>,
303 scope: atlas::AtlasScope,
304 full: bool,
305 ) -> ContextPack {
306 let budget = budget.unwrap_or(self.settings.atlas_tokens);
307 let epoch = self.store.cache_epoch().unwrap_or_else(|_| "no-epoch".into());
308 let key = {
309 let mut h = blake3::Hasher::new();
310 h.update(b"atlas");
311 h.update(budget.to_string().as_bytes());
312 h.update(format!("{scope:?}").as_bytes());
313 h.update(if full { b"full" } else { b"hard" });
314 h.update(self.settings.rank_salt.as_bytes());
315 h.update(epoch.as_bytes());
316 let mut stale: Vec<&String> = self.stale_paths.iter().collect();
317 stale.sort();
318 for p in stale {
319 h.update(p.as_bytes());
320 h.update(b"\0");
321 }
322 format!("atlas:{}", &h.finalize().to_hex()[..20])
323 };
324 if let Ok(Some(cached)) = self.store.cache_get(&key, &epoch) {
325 if let Ok(pack) = serde_json::from_str::<ContextPack>(&cached) {
326 return pack;
327 }
328 }
329 let atlas = atlas::build_atlas_scoped(self, scope);
330 let pack = atlas::render_atlas(self, &atlas, budget, full);
331 if let Ok(json) = serde_json::to_string(&pack) {
332 let _ = self.store.cache_put(&key, &json, &epoch);
333 }
334 pack
335 }
336
337 pub fn task_context(
338 &self,
339 goal: &str,
340 files: &[String],
341 symbols: &[String],
342 token_budget: Option<usize>,
343 ) -> ContextPack {
344 self.task_context_with_rankers(goal, files, symbols, token_budget, None, None)
345 }
346
347 pub fn task_context_with_rankers(
350 &self,
351 goal: &str,
352 files: &[String],
353 symbols: &[String],
354 token_budget: Option<usize>,
355 scorer: Option<&dyn crate::rank::SemanticScorer>,
356 reranker: Option<&dyn crate::rank::Reranker>,
357 ) -> ContextPack {
358 let budget = token_budget.unwrap_or(self.settings.task_tokens).max(512);
359 let epoch = self.store.cache_epoch().unwrap_or_else(|_| "no-epoch".into());
365 let key = {
366 let mut h = blake3::Hasher::new();
367 h.update(goal.as_bytes());
368 for f in files {
369 h.update(f.as_bytes());
370 }
371 for s in symbols {
372 h.update(s.as_bytes());
373 }
374 h.update(budget.to_string().as_bytes());
375 h.update(self.settings.rank_salt.as_bytes());
376 h.update(match self.settings.pack_allocator {
377 PackAllocator::AdaptivePriority => b"alloc:adaptive",
378 PackAllocator::FixedRollover => b"alloc:rollover",
379 });
380 h.update(epoch.as_bytes());
381 let mut stale: Vec<&String> = self.stale_paths.iter().collect();
382 stale.sort();
383 for p in stale {
384 h.update(p.as_bytes());
385 h.update(b"\0");
386 }
387 format!("task:{}", &h.finalize().to_hex()[..20])
388 };
389 if let Ok(Some(cached)) = self.store.cache_get(&key, &epoch) {
390 if let Ok(pack) = serde_json::from_str::<ContextPack>(&cached) {
391 return pack;
392 }
393 }
394 let pack =
395 packs::task_with_rankers(self, goal, files, symbols, budget, scorer, reranker);
396 if let Ok(json) = serde_json::to_string(&pack) {
397 let _ = self.store.cache_put(&key, &json, &epoch);
398 }
399 pack
400 }
401
402 pub fn component_context_full(&self, id: &str) -> ContextPack {
404 packs::component(self, id, self.settings.detail_tokens, true)
405 }
406
407pub fn component_context(&self, id: &str) -> ContextPack {
409 packs::component(self, id, self.settings.detail_tokens, false)
410 }
411
412 pub fn flow_context_full(&self, id: &str) -> ContextPack {
414 packs::flow(self, id, self.settings.detail_tokens, true)
415 }
416
417pub fn flow_context(&self, id: &str) -> ContextPack {
419 packs::flow(self, id, self.settings.detail_tokens, false)
420 }
421
422 pub fn impact_context(
424 &self,
425 files: &[String],
426 symbols: &[String],
427 diff_base: Option<&str>,
428 ) -> ContextPack {
429 packs::impact(self, files, symbols, diff_base, self.settings.detail_tokens, false)
430 }
431
432pub fn impact_context_full(
434 &self,
435 files: &[String],
436 symbols: &[String],
437 diff_base: Option<&str>,
438 ) -> ContextPack {
439 packs::impact(self, files, symbols, diff_base, self.settings.detail_tokens, true)
440 }
441
442 pub fn verify_context_full(&self) -> ContextPack {
444 packs::verify(self, self.settings.detail_tokens, true)
445 }
446
447pub fn verify_context(&self) -> ContextPack {
449 packs::verify(self, self.settings.detail_tokens, false)
450 }
451}