1use crate::surface::{build_surface, build_surface_cached, SurfaceMode, SurfacePolicy, SurfaceRequest};
10use crate::ContextCompiler;
11use scc_core::kinds;
12use scc_core::{estimate_tokens, ContextArtifact, ContextBudget, ContextLedger};
13use serde::{Deserialize, Serialize};
14use std::collections::{BTreeMap, BTreeSet};
15
16pub const RENDERER_VERSION: &str = env!("CARGO_PKG_VERSION");
19
20pub struct StartupContext {
26 pub atlas: String,
27 pub atlas_budget_used: usize,
32 pub skeleton: String,
35 pub surface: String,
36 pub important: String,
40 pub surface_render: scc_core::SurfaceRenderResult,
41 pub coverage: Vec<String>,
42 pub omissions: Vec<String>,
43 pub artifact: ContextArtifact,
44}
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct GlobalRankCache {
63 pub epoch: String,
65 pub policy: String,
67 pub salt: String,
69 pub global_vector: Vec<f64>,
72 pub node_symbol_map: BTreeMap<String, f64>,
75 pub candidates_epoch: String,
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub candidate_ids: Vec<String>,
82 #[serde(default)]
85 pub hits: u64,
86}
87
88pub fn allocate_startup_budget(
102 compiler: &ContextCompiler,
103 target_tokens: Option<usize>,
104) -> ContextBudget {
105 let total = target_tokens.unwrap_or(compiler.settings.startup_tokens);
106 let view = &compiler.view;
107 let entity_count = view.entities().count();
108 let component_count = view.components().len();
109 let flow_count = view.flows().len();
110 let surface_candidates = view.entities_of_kind(scc_core::kinds::SYMBOL).len();
111 scc_core::ContextBudget::adaptive(total, entity_count, component_count, flow_count, surface_candidates)
112}
113
114fn coverage_lines(
116 compiler: &ContextCompiler,
117 render_ids: usize,
118 candidates: usize,
119 render_tokens: usize,
120 surface_budget: usize,
121) -> Vec<String> {
122 let mut coverage = Vec::new();
123 for w in compiler_warnings(compiler) {
124 coverage.push(w);
125 }
126 let mut stale: Vec<String> = compiler.stale_paths.clone();
127 stale.sort();
128 stale.dedup();
129 const MAX_STALE_LINES: usize = 10;
132 for p in stale.iter().take(MAX_STALE_LINES) {
133 coverage.push(format!("stale: {p}"));
134 }
135 if stale.len() > MAX_STALE_LINES {
136 coverage.push(format!(
137 "stale: …and {} more changed file(s) not yet re-indexed",
138 stale.len() - MAX_STALE_LINES
139 ));
140 }
141 coverage.push(format!(
142 "surface map: {} of {} entries rendered, {} tokens (budget {})",
143 render_ids,
144 candidates,
145 render_tokens,
146 surface_budget
147 ));
148 coverage
149}
150
151fn omission_lines(
156 render_omissions: &[scc_core::SurfaceOmission],
157 omitted_len: usize,
158 atlas_dropped: &[String],
159 atlas_hard_truncated: bool,
160 atlas_exceeded: bool,
161) -> Vec<String> {
162 let mut omissions = Vec::new();
163 for o in render_omissions {
164 omissions.push(format!("surface: {} ({})", o.kind, o.reason));
165 }
166 if omitted_len > 0 {
167 omissions.push(format!(
168 "surface: {} lower-ranked definitions omitted",
169 omitted_len
170 ));
171 }
172 for d in atlas_dropped {
173 omissions.push(format!("atlas section dropped: {d}"));
174 }
175 if atlas_hard_truncated {
176 omissions.push("atlas hard-truncated mid-section".into());
177 }
178 if atlas_exceeded {
179 omissions.push("atlas exceeded soft budget: kept complete, over budget".into());
180 }
181 if omissions.is_empty() {
182 omissions.push("none".into());
183 }
184 omissions
185}
186
187pub fn build_startup(
194 compiler: &ContextCompiler,
195 budget: &ContextBudget,
196 renderer_version: &str,
197) -> StartupContext {
198 let epoch = compiler
199 .store
200 .cache_epoch()
201 .unwrap_or_else(|_| "no-epoch".into());
202
203 let atlas_pack = compiler.system_atlas(Some(budget.atlas));
206 let atlas = atlas_pack.content.clone();
207
208 let paths: Vec<String> = compiler
213 .store
214 .all_files()
215 .unwrap_or_default()
216 .into_iter()
217 .map(|(p, _, _, _, _)| p)
218 .collect();
219 let mut skeleton =
220 crate::skeleton::build_skeleton(&paths, crate::skeleton::skeleton_budget(budget.total)).text;
221
222 let mut rank_cache = load_global_rank_cache(compiler);
230 let cache_hit = rank_cache.is_some();
231 let render = build_surface_cached(
232 compiler,
233 SurfaceRequest {
234 mode: SurfaceMode::Global,
235 budget: budget.surface,
236 explain: false,
237 policy: SurfacePolicy::defaults(budget.surface),
238 semantic: None,
239 },
240 &mut rank_cache,
241 );
242 let startup_hard_max = budget.total.saturating_add((budget.total / 5).max(500));
249
250 let mut render = render;
251 let mut atlas_pack = atlas_pack;
252 let mut atlas = atlas;
253 let mut atlas_budget = budget.atlas;
254 let mut coverage = coverage_lines(
259 compiler,
260 render.rendered_ids.len(),
261 render.rendered_ids.len() + render.omitted_ids.len(),
262 render.token_count,
263 budget.surface,
264 );
265
266 const BLOCK_HEADER_OVERHEAD: usize = 40; let mut iterations = 0;
289 loop {
290 let omissions_probe = omission_lines(
291 &render.omissions,
292 render.omitted_ids.len(),
293 &atlas_pack.dropped_sections,
294 atlas_pack.hard_truncated,
295 atlas_pack.exceeded_soft_budget,
296 );
297 let fused = assemble_body(&atlas, &skeleton, &render.text, "", &coverage, &omissions_probe);
298 if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) <= startup_hard_max {
299 break;
300 }
301 iterations += 1;
302 if iterations > 64 {
303 break;
309 }
310 let overhead = estimate_tokens(&assemble_body(&atlas, &skeleton, "", "", &coverage, &omissions_probe));
311 let room = startup_hard_max.saturating_sub(overhead);
312 if room >= 64 {
313 let policy = SurfacePolicy {
316 quotas: true,
317 mmr: true,
318 coverage: true,
319 hard_max: room,
320 };
321 render = build_surface_cached(
322 compiler,
323 SurfaceRequest {
324 mode: SurfaceMode::Global,
325 budget: room,
326 explain: false,
327 policy,
328 semantic: None,
329 },
330 &mut rank_cache,
331 );
332 coverage = coverage_lines(
333 compiler,
334 render.rendered_ids.len(),
335 render.rendered_ids.len() + render.omitted_ids.len(),
336 render.token_count,
337 room,
338 );
339 } else {
340 let smaller = atlas_budget.saturating_mul(3) / 4;
342 if smaller == atlas_budget {
343 break; }
345 atlas_budget = smaller;
346 atlas_pack = compiler.system_atlas(Some(atlas_budget));
347 atlas = atlas_pack.content.clone();
348 }
349 }
350
351 if let Some(c) = &mut rank_cache {
355 if cache_hit {
356 c.hits += 1;
357 }
358 store_global_rank_cache(compiler, c);
359 }
360 let mut surface = render.text.clone();
361 let mut atlas_budget_used = atlas_budget;
362
363 let emergency_omissions: Option<Vec<String>>;
373 {
374 let omissions_probe0 = omission_lines(
375 &render.omissions,
376 render.omitted_ids.len(),
377 &atlas_pack.dropped_sections,
378 atlas_pack.hard_truncated,
379 atlas_pack.exceeded_soft_budget,
380 );
381 if BLOCK_HEADER_OVERHEAD
382 + estimate_tokens(&assemble_body(
383 &atlas,
384 &skeleton,
385 &surface,
386 "",
387 &coverage,
388 &omissions_probe0,
389 ))
390 <= startup_hard_max
391 {
392 emergency_omissions = None;
393 } else {
394
395 atlas_budget_used = 256;
396 atlas_pack = compiler.system_atlas(Some(atlas_budget_used));
397 atlas = atlas_pack.content.clone();
398 render.rendered_ids.clear();
399 render.omitted_ids.clear();
400 render.omissions.clear();
401 render.text = String::new();
402 render.token_count = 0;
403 surface = String::new();
404 coverage = compiler_warnings(compiler);
407 if compiler.stale_paths.is_empty() {
408 coverage.push("model: current".into());
409 } else {
410 coverage.push(format!(
411 "model stale: {} changed file(s) not yet re-indexed (paths omitted over hard max)",
412 compiler.stale_paths.len()
413 ));
414 }
415 coverage.push("surface map: omitted over hard max (0 rendered)".into());
416 let mut skel_budget = crate::skeleton::skeleton_budget(budget.total);
417 loop {
418 let probe = crate::skeleton::build_skeleton(&paths, skel_budget);
419 let omissions_probe = {
420 let mut o = omission_lines(
421 &[],
422 0,
423 &atlas_pack.dropped_sections,
424 atlas_pack.hard_truncated,
425 atlas_pack.exceeded_soft_budget,
426 );
427 o.push(
428 "startup emergency compression: atlas essentials + skeleton only (surface omitted over hard max)".into(),
429 );
430 o };
431 let fused = assemble_body(&atlas, &probe.text, "", "", &coverage, &omissions_probe);
432 if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) <= startup_hard_max
433 || skel_budget == 0
434 {
435 skeleton = probe.text;
436 emergency_omissions = Some(omissions_probe);
437 break;
438 }
439 skel_budget /= 2;
440 }
441 }
442 }
443
444 let important_n = (startup_hard_max / 500).clamp(1, 15);
455 let mut important = if render.rendered_ids.is_empty() {
456 "## SYSTEM-CRITICAL SYMBOLS\n(surface omitted over hard max)\n".to_string()
457 } else {
458 let top = crate::surface::important_symbols(
459 compiler,
460 crate::surface::SurfaceMode::Global,
461 important_n,
462 );
463 crate::surface::render_important(&top, false)
464 };
465 {
466 let probe_om = omission_lines(
467 &render.omissions,
468 render.omitted_ids.len(),
469 &atlas_pack.dropped_sections,
470 atlas_pack.hard_truncated,
471 atlas_pack.exceeded_soft_budget,
472 );
473 let fused = assemble_body(&atlas, &skeleton, &render.text, &important, &coverage, &probe_om);
474 if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) > startup_hard_max && !render.rendered_ids.is_empty() {
475 let over = BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) - startup_hard_max;
476 let room = render
477 .token_count
478 .saturating_sub(over)
479 .saturating_sub(estimate_tokens(&important));
480 if room >= 64 {
481 let policy = SurfacePolicy {
482 quotas: true,
483 mmr: true,
484 coverage: true,
485 hard_max: room,
486 };
487 render = build_surface_cached(
488 compiler,
489 SurfaceRequest {
490 mode: SurfaceMode::Global,
491 budget: room,
492 explain: false,
493 policy,
494 semantic: None,
495 },
496 &mut rank_cache,
497 );
498 coverage = coverage_lines(
499 compiler,
500 render.rendered_ids.len(),
501 render.rendered_ids.len() + render.omitted_ids.len(),
502 render.token_count,
503 room,
504 );
505 important = if render.rendered_ids.is_empty() {
506 "## SYSTEM-CRITICAL SYMBOLS\n(surface omitted over hard max)\n".to_string()
507 } else {
508 let top = crate::surface::important_symbols(
509 compiler,
510 crate::surface::SurfaceMode::Global,
511 important_n,
512 );
513 crate::surface::render_important(&top, false)
514 };
515 let probe_om2 = omission_lines(
518 &render.omissions,
519 render.omitted_ids.len(),
520 &atlas_pack.dropped_sections,
521 atlas_pack.hard_truncated,
522 atlas_pack.exceeded_soft_budget,
523 );
524 let fused2 = assemble_body(&atlas, &skeleton, &render.text, &important, &coverage, &probe_om2);
525 if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused2) > startup_hard_max {
526 important = "## SYSTEM-CRITICAL SYMBOLS\n(omitted over hard max)\n".to_string();
527 }
528 }
529 } else {
530 important = "## SYSTEM-CRITICAL SYMBOLS\n(omitted over hard max)\n".to_string();
532 }
533 }
534
535 let omissions = emergency_omissions.unwrap_or_else(|| {
539 omission_lines(
540 &render.omissions,
541 render.omitted_ids.len(),
542 &atlas_pack.dropped_sections,
543 atlas_pack.hard_truncated,
544 atlas_pack.exceeded_soft_budget,
545 )
546 });
547
548 let trust_policy = trust_policy_str(compiler.view.policy());
549
550 let mut h = blake3::Hasher::new();
554 h.update(b"startup-artifact-v1");
555 h.update(epoch.as_bytes());
556 h.update(renderer_version.as_bytes());
557 h.update(trust_policy.as_bytes());
558 h.update(budget.total.to_string().as_bytes());
559 h.update(budget.atlas.to_string().as_bytes());
560 h.update(budget.surface.to_string().as_bytes());
561 h.update(budget.task_delta.to_string().as_bytes());
562 h.update(budget.structural_source.to_string().as_bytes());
563 let sha256 = h.finalize().to_hex().to_string();
564
565 let body = assemble_body(&atlas, &skeleton, &surface, &important, &coverage, &omissions);
570 let mut ch = blake3::Hasher::new();
571 ch.update(b"startup-content-v1");
572 ch.update(epoch.as_bytes());
573 ch.update(renderer_version.as_bytes());
574 ch.update(trust_policy.as_bytes());
575 ch.update(budget.total.to_string().as_bytes());
576 ch.update(budget.atlas.to_string().as_bytes());
577 ch.update(budget.surface.to_string().as_bytes());
578 ch.update(budget.task_delta.to_string().as_bytes());
579 ch.update(budget.structural_source.to_string().as_bytes());
580 ch.update(body.as_bytes());
581 let content_hash = ch.finalize().to_hex().to_string();
582
583 let mut artifact = ContextArtifact {
584 kind: "startup".into(),
585 epoch,
586 renderer_version: renderer_version.to_string(),
587 trust_policy,
588 budget: budget.clone(),
589 sha256,
590 content_hash,
591 text: String::new(),
592 };
593 artifact.text = assemble_block(&atlas, &skeleton, &surface, &important, &coverage, &omissions, &artifact);
594
595 StartupContext {
596 atlas,
597 atlas_budget_used,
598 skeleton,
599 surface,
600 important,
601 surface_render: render,
602 coverage,
603 omissions,
604 artifact,
605 }
606}
607fn global_rank_key(epoch: &str, policy: &str, salt: &str, pipeline: &str) -> String {
614 let mut h = blake3::Hasher::new();
615 h.update(b"rank:global:v1");
616 h.update(epoch.as_bytes());
617 h.update(b"\0");
618 h.update(policy.as_bytes());
619 h.update(b"\0");
620 h.update(salt.as_bytes());
621 h.update(b"\0");
622 h.update(pipeline.as_bytes());
623 format!("rank:global:{}", &h.finalize().to_hex()[..20])
624}
625
626
627pub fn load_global_rank_cache_with_pipeline(compiler: &ContextCompiler, pipeline: &str) -> Option<GlobalRankCache> {
638 let epoch = compiler.store.cache_epoch().ok()?;
639 let policy = trust_policy_str(compiler.view.policy());
640 let salt = &compiler.settings.rank_salt;
641 let key = global_rank_key(&epoch, &policy, salt, pipeline);
642 let cached = compiler.store.cache_get(&key, &epoch).ok().flatten()?;
643 let c: GlobalRankCache = serde_json::from_str(&cached).ok()?;
644 if c.epoch != epoch || c.policy != policy || c.salt != *salt {
645 return None;
646 }
647 Some(c)
648}
649
650pub fn load_global_rank_cache(compiler: &ContextCompiler) -> Option<GlobalRankCache> {
652 load_global_rank_cache_with_pipeline(compiler, "")
653}
654
655pub fn store_global_rank_cache_with_pipeline(compiler: &ContextCompiler, cache: &GlobalRankCache, pipeline: &str) {
660 let epoch = compiler
661 .store
662 .cache_epoch()
663 .unwrap_or_else(|_| "no-epoch".into());
664 let policy = trust_policy_str(compiler.view.policy());
665 let salt = &compiler.settings.rank_salt;
666 let key = global_rank_key(&epoch, &policy, salt, pipeline);
667 if let Ok(json) = serde_json::to_string(cache) {
668 let _ = compiler.store.cache_put(&key, &json, &epoch);
669 }
670}
671
672pub fn store_global_rank_cache(compiler: &ContextCompiler, cache: &GlobalRankCache) {
674 store_global_rank_cache_with_pipeline(compiler, cache, "")
675}
676
677fn symbol_id_of(entry_id: &str) -> String {
683 entry_id
684 .rsplit_once("#overload")
685 .map(|(logical, _)| logical)
686 .unwrap_or(entry_id)
687 .to_string()
688}
689
690pub fn render_startup(s: &StartupContext) -> String {
694 assemble_block(&s.atlas, &s.skeleton, &s.surface, &s.important, &s.coverage, &s.omissions, &s.artifact)
695}
696
697fn assemble_body(atlas: &str, skeleton: &str, surface: &str, important: &str, coverage: &[String], omissions: &[String]) -> String {
701 let mut out = String::new();
702 out.push_str("## HOW TO READ THIS PACK\n");
703 out.push_str("Machine-generated system context; work within it, do not re-derive.\n");
704 out.push_str("ATLAS=architecture, SKELETON=file layout, SURFACE=ranked APIs, SYMBOLS=top attention, COVERAGE=warnings, OMISSIONS=budget cuts.\n");
705 out.push_str("Authority: source/runtime > pack > checkpoint > hindsight > assumption. DOCUMENTATION labels are unverified. Verify: `scc verify`; stale: `scc index`.\n");
706 out.push_str("Next: `scc context task <goal>`, `scc impact <files>`, `scc drift`, `scc ci check`.\n");
707 out.push_str("\n## SYSTEM ATLAS\n");
708 out.push_str(atlas.trim_end());
709 out.push_str("\n\n## REPOSITORY SKELETON\n");
710 out.push_str(skeleton.trim_end());
711 out.push_str("\n\n## SYSTEM SURFACE MAP\n");
712 out.push_str(surface.trim_end());
713 out.push_str("\n\n");
714 out.push_str(important.trim_end());
715 out.push_str("\n\n## MODEL COVERAGE\n");
716 if coverage.is_empty() {
717 out.push_str("(no warnings)\n");
718 } else {
719 for c in coverage {
720 out.push_str(c);
721 out.push('\n');
722 }
723 }
724 out.push_str("\n## OMISSIONS\n");
725 for o in omissions {
726 out.push_str(o);
727 out.push('\n');
728 }
729 out
730}
731
732fn assemble_block(
734 atlas: &str,
735 skeleton: &str,
736 surface: &str,
737 important: &str,
738 coverage: &[String],
739 omissions: &[String],
740 artifact: &ContextArtifact,
741) -> String {
742 let mut out = String::new();
743 out.push_str("# SCC SYSTEM CONTEXT\n");
744 out.push_str(&format!(
745 "<!-- artifact sha256:{} content_hash:{} epoch:{} renderer:{} -->\n\n",
746 artifact.sha256, artifact.content_hash, artifact.epoch, artifact.renderer_version
747 ));
748 out.push_str(&assemble_body(atlas, skeleton, surface, important, coverage, omissions));
749 out
750}
751
752fn compiler_warnings(compiler: &ContextCompiler) -> Vec<String> {
756 let mut w = Vec::new();
757 if compiler.store.snapshot_status().ok().flatten().is_none() {
758 w.push("Repository is not indexed — run `scc index`.".into());
759 }
760 if !compiler.stale_paths.is_empty() {
761 w.push(format!(
762 "Model is stale: {} changed file(s) not yet re-indexed.",
763 compiler.stale_paths.len()
764 ));
765 }
766 if let Ok(findings) = compiler.store.drift_findings(true) {
767 for (_, kind, sev, msg, _) in findings {
768 if sev == "high" || sev == "critical" {
769 w.push(format!("Drift [{kind}]: {msg}"));
770 }
771 }
772 }
773 w.truncate(6);
774 w
775}
776
777pub fn trust_policy_str(p: &scc_graph::TrustPolicy) -> String {
780 format!(
781 "extracted={} resolved={} observed={} declared={} inferred={} floor={}",
782 p.allow_extracted,
783 p.allow_resolved,
784 p.allow_observed,
785 p.allow_declared,
786 p.allow_inferred,
787 p.min_inferred_confidence
788 )
789}
790
791pub fn visible_ids_from_startup(
800 compiler: &ContextCompiler,
801 startup: &StartupContext,
802) -> (BTreeSet<String>, BTreeSet<String>, BTreeSet<String>, BTreeSet<String>) {
803 let mut symbols = BTreeSet::new();
804 let mut files = BTreeSet::new();
805 let mut components = BTreeSet::new();
806 let mut flows = BTreeSet::new();
807
808 let atlas_pack = compiler.system_atlas(Some(startup.atlas_budget_used));
812
813 let view = &compiler.view;
819 let rendered_symbols: BTreeSet<String> = startup
820 .surface_render
821 .rendered_ids
822 .iter()
823 .map(|id| symbol_id_of(id))
824 .collect();
825 let mut comp_of: BTreeMap<String, String> = BTreeMap::new();
829 for c in view.components() {
830 for r in sorted_rels(view.out_pred(&c.id, scc_core::predicates::CONTAINS)) {
831 for sr in sorted_rels(view.out_pred(&r.object, scc_core::predicates::CONTAINS)) {
832 if rendered_symbols.contains(&sr.object) {
833 comp_of
834 .entry(sr.object.clone())
835 .or_insert_with(|| c.name.clone());
836 }
837 }
838 }
839 }
840 for id in &startup.surface_render.rendered_ids {
841 let symbol_id = symbol_id_of(id);
842 symbols.insert(symbol_id.clone());
843 let ent = view.entity(id).or_else(|| view.entity(&symbol_id));
846 if let Some(e) = ent {
847 if let Some(f) = e.attributes.get("file").and_then(|v| v.as_str()) {
848 files.insert(f.to_string());
849 }
850 }
851 if let Some(c) = comp_of.get(&symbol_id) {
852 components.insert(c.clone());
853 }
854 }
855 for id in &atlas_pack.entity_ids {
857 if let Some(e) = compiler.view.entity(id) {
858 match e.kind.as_str() {
859 kinds::SYMBOL => {
860 symbols.insert(id.clone());
861 }
862 kinds::FILE => {
863 files.insert(e.name.clone());
864 }
865 kinds::COMPONENT => {
866 components.insert(id.clone());
867 }
868 kinds::FLOW => {
869 flows.insert(id.clone());
870 }
871 _ => {}
872 }
873 }
874 }
875 (symbols, files, components, flows)
876}
877
878fn sorted_rels(rels: Vec<&scc_core::Relationship>) -> Vec<&scc_core::Relationship> {
882 let mut v = rels;
883 v.sort_by(|a, b| {
884 a.id.cmp(&b.id)
885 .then_with(|| a.subject.cmp(&b.subject))
886 .then_with(|| a.object.cmp(&b.object))
887 });
888 v
889}
890
891pub fn task_delta_with_ids(
901 compiler: &ContextCompiler,
902 goal: &str,
903 visible: &ContextLedger,
904 budget_tokens: usize,
905 semantic: Option<&dyn crate::rank::SemanticScorer>,
906) -> (String, Vec<String>) {
907 let render = build_surface(
908 compiler,
909 SurfaceRequest {
910 mode: SurfaceMode::Task {
911 goal,
912 visible: Some(visible),
913 },
914 budget: budget_tokens,
915 explain: false,
916 policy: SurfacePolicy::defaults(budget_tokens),
917 semantic,
918 },
919 );
920 let critical = crate::surface::important_symbols(
923 compiler,
924 SurfaceMode::Task { goal, visible: Some(visible) },
925 8,
926 );
927 let mut out = String::new();
928 out.push_str("# SCC TASK DELTA\n");
929 out.push_str(&format!("TASK-FOCUS: {goal}\n"));
930 out.push_str(crate::surface::render_important(&critical, true).as_str());
931 out.push_str("Relevant APIs not already visible:\n");
932 let body = render
933 .text
934 .strip_prefix("SCC SYSTEM SURFACE MAP\n\n")
935 .unwrap_or(&render.text);
936 out.push_str(body.trim_end());
937 out.push('\n');
938 (out, render.rendered_ids)
939}
940
941pub fn task_surface(
946 compiler: &ContextCompiler,
947 goal: &str,
948 budget_tokens: usize,
949 explain: bool,
950 semantic: Option<&dyn crate::rank::SemanticScorer>,
951) -> String {
952 task_surface_with_ids(compiler, goal, budget_tokens, explain, semantic).0
953}
954
955pub fn task_surface_with_ids(
960 compiler: &ContextCompiler,
961 goal: &str,
962 budget_tokens: usize,
963 explain: bool,
964 semantic: Option<&dyn crate::rank::SemanticScorer>,
965) -> (String, Vec<String>) {
966 let render = build_surface(
967 compiler,
968 SurfaceRequest {
969 mode: SurfaceMode::Task {
970 goal,
971 visible: None,
972 },
973 budget: budget_tokens,
974 explain,
975 policy: SurfacePolicy::defaults(budget_tokens),
976 semantic,
977 },
978 );
979 let mut out = String::new();
980 out.push_str(&format!("# SYSTEM SURFACE MAP (task-personalized: {goal})\n"));
981 let body = render
982 .text
983 .strip_prefix("SCC SYSTEM SURFACE MAP\n\n")
984 .unwrap_or(&render.text);
985 out.push_str(body.trim_end());
986 out.push('\n');
987 (out, render.rendered_ids)
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993
994fn fixture_compiler() -> (tempfile::TempDir, crate::ContextCompiler<'static>) {
996 let dir = tempfile::TempDir::new().unwrap();
997 let root = dir.path().join("repo");
998 std::fs::create_dir_all(&root).unwrap();
999 let store = Box::leak(Box::new(
1000 scc_store::Store::open(&dir.path().join("scc.db"), &root).unwrap(),
1001 ));
1002 let graph = Box::leak(Box::new(scc_graph::RealityGraph::load(store).unwrap()));
1003 let settings = crate::ContextSettings::default();
1004 let comp = crate::ContextCompiler::new(store, graph, settings, Vec::new());
1005 (dir, comp)
1006 }
1007
1008 #[test]
1009fn render_startup_emits_spec_headers() {
1011 let sc = StartupContext {
1012 atlas: "ATLAS-BODY".into(),
1013 atlas_budget_used: 6000,
1014 skeleton: "SKELETON-BODY".into(),
1015 surface: "SURFACE-BODY".into(),
1016 important: "IMPORTANT-BODY".into(),
1017 surface_render: scc_core::SurfaceRenderResult {
1018 text: "SURFACE-BODY".into(),
1019 rendered_ids: vec![],
1020 rendered_entries: vec![],
1021 omitted_ids: vec![],
1022 omissions: vec![],
1023 token_count: 0,
1024 critical_drops: vec![],
1025 },
1026 coverage: vec!["stale: a.py".into()],
1027 omissions: vec!["none".into()],
1028 artifact: ContextArtifact {
1029 kind: "startup".into(),
1030 epoch: "epoch:test".into(),
1031 renderer_version: "test".into(),
1032 trust_policy: "floor=0.85".into(),
1033 budget: ContextBudget::default(),
1034 sha256: "abc".into(),
1035 content_hash: "def".into(),
1036 text: String::new(),
1037 },
1038 };
1039 let out = render_startup(&sc);
1040 assert!(out.contains("# SCC SYSTEM CONTEXT"));
1041 assert!(out.contains("## SYSTEM ATLAS"));
1042 assert!(out.contains("## REPOSITORY SKELETON"));
1043 assert!(out.contains("## SYSTEM SURFACE MAP"));
1044 assert!(out.contains("ATLAS-BODY"));
1045 assert!(out.contains("SURFACE-BODY"));
1046 assert!(out.contains("SKELETON-BODY"));
1047 assert!(out.contains("## SYSTEM-CRITICAL SYMBOLS") || out.contains("IMPORTANT-BODY"));
1048 let (ia, ik, is) = (
1049 out.find("## SYSTEM ATLAS").unwrap(),
1050 out.find("## REPOSITORY SKELETON").unwrap(),
1051 out.find("## SYSTEM SURFACE MAP").unwrap(),
1052 );
1053 assert!(ia < ik && ik < is, "skeleton grounds before architecture");
1054 assert!(out.contains("stale: a.py"));
1055 }
1056
1057 #[test]
1058fn artifact_text_equals_rendered_block() {
1060 let (_dir, comp) = fixture_compiler();
1061 let budget = ContextBudget::default();
1062 let sc = build_startup(&comp, &budget, "test-renderer");
1063 assert_eq!(sc.artifact.text, render_startup(&sc));
1064 assert_eq!(sc.artifact.sha256.len(), 64);
1065 assert_eq!(sc.artifact.content_hash.len(), 64);
1066 assert_ne!(sc.artifact.content_hash, sc.artifact.sha256);
1067 assert!(render_startup(&sc).contains("content_hash:"));
1068 assert_eq!(sc.artifact.epoch, comp.store.cache_epoch().unwrap_or_default());
1069 }
1070
1071 #[test]
1072fn global_rank_cache_roundtrips_through_the_store() {
1074 let (_dir, comp) = fixture_compiler();
1075 assert!(load_global_rank_cache(&comp).is_none());
1077 let cache = GlobalRankCache {
1078 epoch: comp.store.cache_epoch().unwrap_or_else(|_| "no-epoch".into()),
1079 policy: trust_policy_str(comp.view.policy()),
1080 salt: comp.settings.rank_salt.clone(),
1081 global_vector: vec![0.1, 0.2, 0.3],
1082 node_symbol_map: BTreeMap::from([("repo://r/symbol/a.py/serve".into(), 0.42)]),
1083 candidates_epoch: comp.store.cache_epoch().unwrap_or_default(),
1084 candidate_ids: vec!["repo://r/symbol/a.py/serve".into()],
1085 hits: 1,
1086 };
1087 store_global_rank_cache(&comp, &cache);
1088 let loaded = load_global_rank_cache(&comp).expect("cache entry present after store");
1089 assert_eq!(loaded, cache);
1090 assert_eq!(loaded.hits, 1);
1091 assert_eq!(
1092 loaded.node_symbol_map.get("repo://r/symbol/a.py/serve"),
1093 Some(&0.42)
1094 );
1095 assert_eq!(loaded.candidates_epoch, loaded.epoch);
1096 }
1097
1098 #[test]
1099fn visible_ids_consume_the_same_render_the_artifact_printed() {
1101 let (_dir, comp) = fixture_compiler();
1108 let budget = ContextBudget::default();
1109 let sc = build_startup(&comp, &budget, "test-renderer");
1110 assert!(sc.artifact.text.contains(&sc.surface));
1111 let (syms, _files, _comps, _flows) = visible_ids_from_startup(&comp, &sc);
1112 for id in &sc.surface_render.rendered_ids {
1113 let symbol_id = symbol_id_of(id);
1114 assert!(
1115 syms.contains(&symbol_id),
1116 "rendered entry {id} must be marked visible (symbol {symbol_id})"
1117 );
1118 }
1119 }
1120}