1use crate::context_ledger::novelty_penalty;
13use crate::rank::{term_match, terms};
14use crate::ContextCompiler;
15use scc_core::{
16 estimate_tokens, kinds, predicates, ContextItem, ContextLedger, Provenance,
17 SemanticParameter, SemanticSignature, SourceRange, SurfaceEntry, SurfaceKind,
18 SurfaceOmission, SurfaceRank, SystemSurfaceMap, TaskSeed, Visibility,
19};
20use std::collections::{BTreeMap, BTreeSet, HashMap};
21
22const SYMBOL_KINDS: [&str; 9] = [
25 "function", "method", "class", "interface", "trait", "type", "const", "enum", "module",
26];
27
28const REDISTRIBUTION_SCALE: f64 = 1.0 / (1.0 - crate::pagerank::SEMANTIC_WEIGHT);
37
38const MAX_COMPRESSION: u8 = 3;
44
45pub fn compile_surface_map(compiler: &ContextCompiler) -> SystemSurfaceMap {
52 let view = &compiler.view;
53
54 let mut symbol_comp_id: BTreeMap<String, String> = BTreeMap::new();
56 let mut comp_names: BTreeMap<String, String> = BTreeMap::new();
57 for c in view.components() {
58 comp_names.insert(c.id.clone(), c.name.clone());
59 for r in sorted_rels(view.out_pred(&c.id, predicates::CONTAINS)) {
60 for sr in sorted_rels(view.out_pred(&r.object, predicates::CONTAINS)) {
61 symbol_comp_id.insert(sr.object.clone(), c.id.clone());
62 }
63 }
64 }
65 let mut subsys_of_comp: BTreeMap<String, String> = BTreeMap::new();
66 for kind in [kinds::SUBSYSTEM, kinds::SERVICE] {
67 for e in view.entities_of_kind(kind) {
68 for r in sorted_rels(view.out_pred(&e.id, predicates::CONTAINS)) {
69 subsys_of_comp
70 .entry(r.object.clone())
71 .or_insert_with(|| e.name.clone());
72 }
73 }
74 }
75
76 let surfaces = scc_graph::flows::invocation_surfaces(view.graph);
78 let mut surface_by_symbol: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
79 for s in surfaces {
80 surface_by_symbol
81 .entry(s.symbol.clone())
82 .or_default()
83 .push((s.kind.as_str().to_string(), s.trigger.clone()));
84 }
85
86 let all_flows: Vec<scc_core::Flow> = view.flows();
91 let mut flows_by_actor: HashMap<&str, Vec<&str>> = HashMap::new();
97 let mut all_actors: Vec<&str> = Vec::new();
98 for f in &all_flows {
99 for s in &f.steps {
100 if let Some(v) = flows_by_actor.get_mut(s.actor.as_str()) {
101 if !v.contains(&f.name.as_str()) {
102 v.push(f.name.as_str());
103 }
104 } else {
105 all_actors.push(s.actor.as_str());
106 flows_by_actor.insert(s.actor.as_str(), vec![f.name.as_str()]);
107 }
108 }
109 }
110 let mut flows_by_actor_file: HashMap<String, Vec<String>> = HashMap::new();
114 let mut routes_by_handler: HashMap<&str, Vec<(String, String)>> = HashMap::new();
115 for r in view.entities_of_kind(kinds::ROUTE) {
116 if let (Some(h), Some(m), Some(path)) = (
117 r.attributes.get("handler").and_then(|v| v.as_str()),
118 r.attributes.get("method").and_then(|v| v.as_str()),
119 r.attributes.get("path").and_then(|v| v.as_str()),
120 ) {
121 if !path.is_empty() {
122 routes_by_handler
123 .entry(h)
124 .or_default()
125 .push((m.to_string(), path.to_string()));
126 }
127 }
128 }
129
130 let mut entries: Vec<SurfaceEntry> = Vec::new();
131 for e in view.entities_of_kind(kinds::SYMBOL) {
132 let Some(kind_str) = e.attributes.get("kind").and_then(|v| v.as_str()) else {
133 continue;
134 };
135 if !SYMBOL_KINDS.contains(&kind_str) {
136 continue;
137 }
138 entries.push(build_entry(
139 compiler,
140 e,
141 kind_str,
142 &symbol_comp_id,
143 &comp_names,
144 &subsys_of_comp,
145 &surface_by_symbol,
146 &flows_by_actor,
147 &all_actors,
148 &mut flows_by_actor_file,
149 &routes_by_handler,
150 ));
151 }
152 entries.sort_by(|a, b| {
153 a.qualified_name
154 .cmp(&b.qualified_name)
155 .then_with(|| a.id.cmp(&b.id))
156 });
157
158 let store = compiler.store;
159 let mut map = SystemSurfaceMap {
160 repository: store.repository().name,
161 revision: compiler.revision(),
162 epoch: store
163 .cache_epoch()
164 .unwrap_or_else(|_| "no-epoch".into()),
165 entries,
166 token_count: 0,
167 omitted: Vec::new(),
168 };
169 let full = render_surface_map(&map, None);
170 map.token_count = estimate_tokens(&full);
171 map
172}
173
174pub fn render_surface_map(map: &SystemSurfaceMap, budget_tokens: Option<usize>) -> String {
177 let budget_chars = budget_tokens.map(|t| t.saturating_mul(4));
178 let (body, omitted) = render_entry_groups(&map.entries, budget_chars);
179 let mut out = String::from("SCC SYSTEM SURFACE MAP\n\n");
180 out.push_str(&body);
181 if !omitted.is_empty() {
182 out.push('\n');
183 out.push_str("OMITTED (token budget exceeded):\n");
184 for (kind, count) in &omitted {
185 out.push_str(&format!(" {count} lower-ranked {kind} definitions\n"));
186 }
187 }
188 out
189}
190
191fn render_entry_groups(
197 entries: &[SurfaceEntry],
198 budget_chars: Option<usize>,
199) -> (String, BTreeMap<String, usize>) {
200 group_and_render(entries, budget_chars, 0, false)
201}
202
203fn group_and_render(
210 entries: &[SurfaceEntry],
211 budget_chars: Option<usize>,
212 level: u8,
213 explain: bool,
214) -> (String, BTreeMap<String, usize>) {
215 let mut groups: BTreeMap<(String, String, String), Vec<&SurfaceEntry>> = BTreeMap::new();
216 for e in entries {
217 let comp = e.component.clone().unwrap_or_else(|| "(unattributed)".to_string());
218 let sub = e.subsystem.clone().unwrap_or_default();
219 groups.entry((comp, sub, e.path.clone())).or_default().push(e);
220 }
221
222 let mut out = String::new();
223 let mut total_chars = 0usize;
224 let mut omitted: BTreeMap<String, usize> = BTreeMap::new();
225 let mut cut = false;
226
227 for ((comp, sub, path), mut es) in groups {
228 es.sort_by(|a, b| entry_order(a, b));
229 let header = group_header(&comp, &sub, &path);
230 let mut blocks: Vec<String> = Vec::new();
231 let mut block_chars: usize = 0;
232 for e in es {
233 if cut {
234 *omitted.entry(e.kind.as_str().to_string()).or_insert(0) += 1;
235 continue;
236 }
237 let rank = if explain { Some(&e.rank) } else { None };
238 let block = render_entry_opt(e, level, rank);
239 let bc = block.chars().count();
240 if fits(total_chars + header.chars().count() + block_chars + bc, budget_chars) {
241 blocks.push(block);
242 block_chars += bc;
243 } else {
244 cut = true;
245 *omitted.entry(e.kind.as_str().to_string()).or_insert(0) += 1;
246 }
247 }
248 if !blocks.is_empty() {
249 out.push_str(&header);
250 for b in blocks {
251 out.push_str(&b);
252 }
253 total_chars += header.chars().count() + block_chars;
254 }
255 }
256 (out, omitted)
257}
258
259pub fn surface_quotas() -> Vec<(String, f64)> {
268 vec![
269 ("public".to_string(), 0.30),
270 ("core".to_string(), 0.25),
271 ("types".to_string(), 0.15),
272 ("state".to_string(), 0.10),
273 ("contract".to_string(), 0.10),
274 ("flow".to_string(), 0.10),
275 ]
276}
277
278fn quota_kind(e: &SurfaceEntry) -> &'static str {
283 if e.exported || e.visibility == Visibility::Public || !e.invocation_surfaces.is_empty() {
284 "public"
285 } else if !e.state_authorities.is_empty() {
286 "state"
287 } else if !e.contracts.is_empty() {
288 "contract"
289 } else if !e.flows.is_empty() {
290 "flow"
291 } else if matches!(
292 e.kind,
293 SurfaceKind::Class
294 | SurfaceKind::Interface
295 | SurfaceKind::Trait
296 | SurfaceKind::Enum
297 | SurfaceKind::Type
298 ) {
299 "types"
300 } else {
301 "core"
302 }
303}
304
305fn invariant_names(view: &scc_graph::TrustedGraphView) -> Vec<String> {
310 let mut names: Vec<String> = Vec::new();
311 for inv in view.invariants() {
312 let name = inv.id.rsplit('/').next().unwrap_or(&inv.id).to_string();
313 names.push(name);
314 names.push(inv.statement.clone());
315 }
316 names
317}
318
319pub fn required_ids(map: &SystemSurfaceMap, compiler: &ContextCompiler) -> BTreeSet<String> {
327 let view = &compiler.view;
328 let mut required: BTreeSet<String> = BTreeSet::new();
329
330 let inv_names = invariant_names(view);
333 let mut critical_state: BTreeSet<String> = BTreeSet::new();
334 for inv in view.invariants() {
335 for scope_id in &inv.scope {
336 if let Some(e) = view.entity(scope_id) {
337 if e.kind == kinds::STATE {
338 critical_state.insert(scope_id.clone());
339 }
340 }
341 }
342 }
343 let state_name_to_id: BTreeMap<String, String> = view
344 .entities_of_kind(kinds::STATE)
345 .into_iter()
346 .map(|e| (e.name.clone(), e.id.clone()))
347 .collect();
348
349 let mut primary_eps: BTreeSet<String> = BTreeSet::new();
352 for f in view.flows() {
353 let triggered = f.trigger.as_deref().map(|t| !t.is_empty()).unwrap_or(false);
354 if triggered {
355 if let Some(ep) = f.attributes.get("entrypoint").and_then(|v| v.as_str()) {
356 primary_eps.insert(ep.to_string());
357 }
358 }
359 }
360
361 for e in &map.entries {
362 let mut req = e
369 .invocation_surfaces
370 .iter()
371 .any(|s| !s.starts_with("public_api:"));
372 if !req && primary_eps.contains(&e.symbol_id) {
373 req = true;
374 }
375 if !req {
376 req = e.contracts.iter().any(|c| {
377 inv_names
378 .iter()
379 .any(|n| !n.is_empty() && c.to_lowercase().contains(&n.to_lowercase()))
380 });
381 }
382 if !req {
383 req = e.state_authorities.iter().any(|auth| {
384 state_name_to_id
385 .get(auth)
386 .map(|id| critical_state.contains(id))
387 .unwrap_or(false)
388 });
389 }
390 if req {
391 required.insert(e.id.clone());
392 }
393 }
394 required
395}
396
397fn entry_reasons(
404 e: &SurfaceEntry,
405 goal: Option<&str>,
406 seed_ids: &BTreeSet<String>,
407 inv_names: &[String],
408 changed: bool,
409) -> Vec<String> {
410 let mut reasons: Vec<String> = Vec::new();
411 if seed_ids.contains(&e.symbol_id) {
412 let label = e
413 .component
414 .as_deref()
415 .filter(|c| !c.is_empty())
416 .unwrap_or(goal.unwrap_or("task"));
417 reasons.push(format!("task seed: {label}"));
418 }
419 if !e.flows.is_empty() {
420 reasons.push("primary flow participant".into());
421 }
422 if e.exported || e.visibility == Visibility::Public {
423 reasons.push("public component surface".into());
424 }
425 for s in &e.state_authorities {
426 reasons.push(format!("owns {s}"));
427 }
428 if e.contracts.iter().any(|c| {
429 inv_names
430 .iter()
431 .any(|n| !n.is_empty() && c.to_lowercase().contains(&n.to_lowercase()))
432 }) {
433 reasons.push("invariant-enforcing".into());
434 }
435 if !e.invocation_surfaces.is_empty() {
436 reasons.push("concrete invocation surface".into());
437 }
438 if changed {
439 reasons.push("change risk: modified path".into());
440 }
441 reasons
442}
443
444fn render_selected(entries: &[SurfaceEntry], level: u8, explain: bool) -> String {
452 let (body, _) = group_and_render(entries, None, level, explain);
453 let mut out = String::from("SCC SYSTEM SURFACE MAP\n\n");
454 if explain {
455 out.push_str("selection scores shown per entry\n\n");
456 }
457 out.push_str(&body);
458 out
459}
460
461
462pub(crate) fn file_importance(path: &str) -> f64 {
469 let base = path.rsplit('/').next().unwrap_or(path);
470 let important = [
471 "package.json", "pnpm-workspace.yaml", "yarn.lock", "Cargo.toml",
473 "Cargo.lock", "go.mod", "pyproject.toml", "setup.py", "setup.cfg",
474 "requirements.txt", "pom.xml", "build.gradle", "build.gradle.kts",
475 "settings.gradle", "settings.gradle.kts", "gradlew", "Makefile",
476 "CMakeLists.txt", "mix.exs", "Gemfile", "composer.json",
477 "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
479 "compose.yml", "compose.yaml", ".dockerignore",
480 ".github/workflows/ci.yml", ".github/workflows/main.yml",
482 ".gitlab-ci.yml", "Jenkinsfile", "azure-pipelines.yml",
483 ".circleci/config.yml", "buildkite.yml",
484 "main.py", "main.go", "main.ts", "index.ts", "index.js",
486 "app.py", "server.py", "server.ts", "server.js", "cli.py",
487 "cli.ts", "cli.go", "src/main.rs", "bin/main.rs", "app.js",
488 "app.ts",
489 ];
490 if important.contains(&base) || important.contains(&path) {
491 return 1.0;
492 }
493 if path.starts_with(".github/workflows/") {
495 return 1.0;
496 }
497 0.0
498}
499
500pub fn entry_lexical(e: &SurfaceEntry, goal_terms: &BTreeSet<String>) -> f64 {
506 if goal_terms.is_empty() {
507 return 0.0;
508 }
509 let name_terms = terms(&e.qualified_name);
510 let sig_terms = terms(&e.source_signature);
511 let name_hits = goal_terms
512 .iter()
513 .filter(|g| name_terms.iter().any(|n| term_match(g, n)))
514 .count();
515 let sig_hits = goal_terms
516 .iter()
517 .filter(|g| sig_terms.iter().any(|n| term_match(g, n)))
518 .count();
519 (name_hits * 2 + sig_hits) as f64
520}
521
522fn finish_selection(
532 map: &SystemSurfaceMap,
533 ranked: Vec<(String, f64)>,
534 required: &BTreeSet<String>,
535 budget: usize,
536 policy: &SurfacePolicy,
537 stages: &SurfacePipelineStages,
538 explain: bool,
539) -> scc_core::SurfaceRenderResult {
540 let entry_of: BTreeMap<String, &SurfaceEntry> =
541 map.entries.iter().map(|e| (e.id.clone(), e)).collect();
542
543 let mut required_items: Vec<(String, f64)> = Vec::new();
546 let mut pool: Vec<(String, f64)> = Vec::new();
547 for (id, imp) in ranked {
548 if policy.coverage && required.contains(&id) {
549 required_items.push((id, imp));
550 } else {
551 pool.push((id, imp));
552 }
553 }
554
555 let required_tokens_full: usize = required_items
560 .iter()
561 .filter_map(|(id, _)| entry_of.get(id))
562 .map(|e| estimate_tokens(&render_entry(e)))
563 .sum();
564 let compress = policy.coverage && required_tokens_full > policy.hard_max;
565
566 let mut pre_cap_critical_drops: Vec<String> = Vec::new();
574 if policy.coverage {
575 let cost_of_c = |id: &str| -> usize {
576 entry_of
577 .get(id)
578 .map(|e| estimate_tokens(&render_entry_compressed(e)))
579 .unwrap_or(1)
580 };
581 let mut spent_c: usize = 0;
582 let mut capped: Vec<(String, f64)> = Vec::new();
583 for (id, imp) in &required_items {
584 let c = cost_of_c(id);
585 if spent_c + c > policy.hard_max && !capped.is_empty() {
586 pre_cap_critical_drops.push(id.clone());
589 continue;
590 }
591 spent_c += c;
592 capped.push((id.clone(), *imp));
593 }
594 if capped.is_empty() && !required_items.is_empty() {
595 capped.push(required_items[0].clone());
599 for (id, _) in &required_items[1..] {
600 if !pre_cap_critical_drops.contains(id) {
601 pre_cap_critical_drops.push(id.clone());
602 }
603 }
604 }
605 if capped.len() < required_items.len() {
606 required_items = capped;
607 }
608 }
609
610 let cost_of = |id: &str| -> usize {
611 match (entry_of.get(id), compress) {
612 (Some(e), true) => estimate_tokens(&render_entry_compressed(e)),
613 (Some(e), false) => estimate_tokens(&render_entry(e)),
614 _ => 1,
615 }
616 };
617 let required_spent: usize = required_items.iter().map(|(id, _)| cost_of(id)).sum();
618 let available = budget.saturating_sub(required_spent);
620
621 let pool_avg = if pool.is_empty() {
624 1
625 } else {
626 (pool.iter().map(|(id, _)| cost_of(id)).sum::<usize>() / pool.len()).max(1)
627 };
628 let mmr_budget = available
629 .saturating_div(pool_avg)
630 .max(1)
631 .min(pool.len());
632 let diversified: Vec<(String, f64)> = if stages.mmr && policy.mmr {
633 let mut comp_ids: HashMap<&str, u32> = HashMap::new();
637 let mut path_ids: HashMap<&str, u32> = HashMap::new();
638 let mut group_of: HashMap<&str, (u32, u32)> = HashMap::new();
639 for (id, e) in &entry_of {
640 let c = match &e.component {
641 Some(c) => {
642 let n = comp_ids.len() as u32;
643 *comp_ids.entry(c.as_str()).or_insert(n + 1)
644 }
645 None => 0,
646 };
647 let g = if e.path.is_empty() {
648 0
649 } else {
650 let n = path_ids.len() as u32;
651 *path_ids.entry(e.path.as_str()).or_insert(n + 1)
652 };
653 group_of.insert(id.as_str(), (c, g));
654 }
655 let sim = |a: &str, b: &str| -> f64 {
656 match (group_of.get(a), group_of.get(b)) {
657 (Some((ca, pa)), Some((cb, pb)))
658 if (*ca != 0 && *ca == *cb) || (*pa != 0 && *pa == *pb) =>
659 {
660 1.0
661 }
662 _ => 0.0,
663 }
664 };
665 let pool_by_id: BTreeMap<String, f64> = pool.iter().cloned().collect();
666 crate::selector::mmr_diversify(&pool, sim, 0.5, mmr_budget)
667 .into_iter()
668 .filter_map(|id| pool_by_id.get(&id).map(|v| (id.clone(), *v)))
669 .collect()
670 } else {
671 pool.clone()
672 };
673
674 let quota_filtered: Vec<(String, f64)> = if stages.quotas && policy.quotas {
679 let kind_of: BTreeMap<String, &'static str> = map
680 .entries
681 .iter()
682 .map(|e| (e.id.clone(), quota_kind(e)))
683 .collect();
684 let ids = crate::selector::enforce_quotas(
685 &diversified,
686 |id| kind_of.get(id).copied().unwrap_or("core"),
687 &surface_quotas(),
688 available,
689 |id| cost_of(id),
690 );
691 let id_set: BTreeSet<String> = ids.into_iter().collect();
692 diversified
693 .into_iter()
694 .filter(|(id, _)| id_set.contains(id))
695 .collect()
696 } else {
697 diversified
698 };
699
700 let mut items: Vec<ContextItem> = Vec::new();
704 for (id, imp) in &required_items {
705 items.push(ContextItem {
706 id: id.clone(),
707 value: *imp,
708 token_cost: cost_of(id),
709 required: true,
710 group: Some("api".into()),
711 });
712 }
713 for (id, imp) in "a_filtered {
714 items.push(ContextItem {
715 id: id.clone(),
716 value: *imp,
717 token_cost: cost_of(id),
718 required: false,
719 group: Some("api".into()),
720 });
721 }
722 let selected = if stages.optimizer {
723 crate::selector::select_with_budget(&items, budget, policy.hard_max)
724 } else {
725 crate::selector::select_in_order(&items, budget, policy.hard_max)
726 };
727
728 let mut rendered_ids: Vec<String> = Vec::new();
729 let mut selected_entries: Vec<SurfaceEntry> = Vec::new();
730 for &idx in &selected {
731 if idx >= items.len() {
732 continue; }
734 let id = items[idx].id.clone();
735 if let Some(e) = entry_of.get(&id) {
736 rendered_ids.push(id.clone());
737 selected_entries.push((*e).clone());
738 }
739 }
740
741 let mut level: u8 = if compress { 1 } else { 0 };
752 let mut critical_drops: Vec<String> = pre_cap_critical_drops;
753 let mut overflow_drops: usize = 0;
754 let mut text = render_selected(&selected_entries, level, explain);
755 let mut token_count = estimate_tokens(&text);
756 while token_count > policy.hard_max {
757 if level < MAX_COMPRESSION {
758 level += 1;
759 } else {
760 let Some(dropped) = selected_entries.pop() else {
761 break; };
763 rendered_ids.retain(|id| id != &dropped.id);
764 if required.contains(&dropped.id) {
765 critical_drops.push(dropped.id.clone());
766 } else {
767 overflow_drops += 1;
768 }
769 }
770 text = render_selected(&selected_entries, level, explain);
771 token_count = estimate_tokens(&text);
772 }
773
774 let rendered_set: BTreeSet<String> = rendered_ids.iter().cloned().collect();
779 let mut omitted_ids: Vec<String> = Vec::new();
780 let mut by_kind: BTreeMap<String, usize> = BTreeMap::new();
781 for e in &map.entries {
782 if rendered_set.contains(&e.id) {
783 continue;
784 }
785 omitted_ids.push(e.id.clone());
786 *by_kind.entry(e.kind.as_str().to_string()).or_insert(0) += 1;
787 }
788 let mut omissions: Vec<SurfaceOmission> = by_kind
789 .into_iter()
790 .map(|(kind, count)| SurfaceOmission {
791 count,
792 kind,
793 reason: "not selected within budget (diversity/quotas/token budget)".into(),
794 })
795 .collect();
796 if overflow_drops > 0 {
797 omissions.push(SurfaceOmission {
798 count: overflow_drops,
799 kind: "required-overflow".into(),
800 reason: "hard-max invariant on final rendered text (lowest-value entries dropped after full compression)".into(),
801 });
802 }
803 scc_core::SurfaceRenderResult {
804 text,
805 rendered_ids,
806 rendered_entries: selected_entries,
807 omitted_ids,
808 omissions,
809 token_count,
810 critical_drops,
811 }
812}
813
814#[derive(Debug, Clone, Copy)]
821pub enum SurfaceMode<'a> {
823 Global,
824 Task {
825 goal: &'a str,
826 visible: Option<&'a ContextLedger>,
830 },
831}
832
833#[derive(Clone, Copy)]
837pub struct SurfaceRequest<'a> {
839 pub mode: SurfaceMode<'a>,
840 pub budget: usize,
841 pub explain: bool,
842 pub policy: SurfacePolicy,
843 pub semantic: Option<&'a dyn crate::rank::SemanticScorer>,
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
853pub struct SurfacePolicy {
855 pub quotas: bool,
857 pub mmr: bool,
859 pub coverage: bool,
861 pub hard_max: usize,
866}
867
868impl SurfacePolicy {
870 pub fn defaults(budget: usize) -> Self {
874 SurfacePolicy {
875 quotas: true,
876 mmr: true,
877 coverage: true,
878 hard_max: budget
879 .saturating_add(budget / 5)
880 .max(budget.saturating_add(500)),
881 }
882 }
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq)]
888pub struct SurfacePipelineStages {
890 pub lexical: bool,
893 pub global_ppr: bool,
895 pub task_ppr: bool,
897 pub mmr: bool,
899 pub quotas: bool,
901 pub optimizer: bool,
904}
905
906impl Default for SurfacePipelineStages {
908 fn default() -> Self {
912 SurfacePipelineStages {
913 lexical: true,
914 global_ppr: true,
915 task_ppr: true,
916 mmr: true,
917 quotas: true,
918 optimizer: true,
919 }
920 }
921}
922
923pub fn build_surface(
936 compiler: &ContextCompiler,
937 request: SurfaceRequest<'_>,
938) -> scc_core::SurfaceRenderResult {
939 build_surface_staged(compiler, request, &SurfacePipelineStages::default())
940}
941
942pub fn build_surface_staged(
951 compiler: &ContextCompiler,
952 request: SurfaceRequest<'_>,
953 stages: &SurfacePipelineStages,
954) -> scc_core::SurfaceRenderResult {
955 build_surface_staged_inner(compiler, request, stages, &mut None)
956}
957
958pub fn build_surface_cached(
973 compiler: &ContextCompiler,
974 request: SurfaceRequest<'_>,
975 cache: &mut Option<crate::startup::GlobalRankCache>,
976) -> scc_core::SurfaceRenderResult {
977 build_surface_staged_inner(
978 compiler,
979 request,
980 &SurfacePipelineStages::default(),
981 cache,
982 )
983}
984
985fn build_surface_staged_inner(
993 compiler: &ContextCompiler,
994 request: SurfaceRequest<'_>,
995 stages: &SurfacePipelineStages,
996 cache: &mut Option<crate::startup::GlobalRankCache>,
997) -> scc_core::SurfaceRenderResult {
998 let (goal, visible): (Option<&str>, Option<&ContextLedger>) = match &request.mode {
999 SurfaceMode::Global => (None, None),
1000 SurfaceMode::Task { goal, visible } => (Some(goal), *visible),
1001 };
1002 let goal_terms = goal.map(terms).unwrap_or_default();
1003
1004 let mut seeds: Vec<TaskSeed> = Vec::new();
1007 let mut seed_ids: BTreeSet<String> = BTreeSet::new();
1008 if stages.task_ppr {
1009 if let Some(g) = goal {
1010 let candidates = crate::rank::collect_lexical_candidates(
1011 compiler.store,
1012 &compiler.view,
1013 g,
1014 &[],
1015 16,
1016 );
1017 seeds = candidates
1018 .iter()
1019 .map(|c| TaskSeed {
1020 kind: c.kind.clone(),
1021 id: c.id.clone(),
1022 weight: c.score,
1023 })
1024 .collect();
1025 seed_ids = seeds.iter().map(|s| s.id.clone()).collect();
1026 }
1027 }
1028
1029 let mut map = compile_surface_map(compiler);
1030 let mut global_of: BTreeMap<String, f64> = BTreeMap::new();
1037 let mut task_ranker: Option<crate::pagerank::SystemRanker<'_>> = None;
1038 if stages.global_ppr {
1039 if let Some(c) = cache.as_ref() {
1040 global_of = c.node_symbol_map.clone();
1041 } else {
1042 let ranker = crate::pagerank::SystemRanker::new(&compiler.view);
1043 let gv = ranker.global_vector();
1044 global_of = ranker.project_to_symbols(&gv).into_iter().collect();
1045 *cache = Some(crate::startup::GlobalRankCache {
1046 epoch: compiler
1047 .store
1048 .cache_epoch()
1049 .unwrap_or_else(|_| "no-epoch".into()),
1050 policy: crate::startup::trust_policy_str(compiler.view.policy()),
1051 salt: compiler.settings.rank_salt.clone(),
1052 global_vector: gv,
1053 node_symbol_map: global_of.clone(),
1054 candidates_epoch: map.epoch.clone(),
1055 candidate_ids: map.entries.iter().map(|e| e.id.clone()).collect(),
1056 hits: 0,
1057 });
1058 task_ranker = Some(ranker);
1059 }
1060 }
1061 let task_of: BTreeMap<String, f64> = if stages.task_ppr && !seeds.is_empty() {
1062 let ranker = match task_ranker {
1067 Some(r) => r,
1068 None => crate::pagerank::SystemRanker::new(&compiler.view),
1069 };
1070 ranker
1071 .project_to_symbols(&ranker.task_vector(&seeds))
1072 .into_iter()
1073 .collect()
1074 } else {
1075 BTreeMap::new()
1076 };
1077
1078 let required = required_ids(&map, compiler);
1079 let has_task = !seeds.is_empty();
1080 let inv_names = invariant_names(&compiler.view);
1081 let mut ranked: Vec<(String, f64)> = Vec::new();
1082 let mut ranks: BTreeMap<String, SurfaceRank> = BTreeMap::new();
1083 for e in &map.entries {
1084 let changed = compiler.is_stale_path(&e.path);
1085 let novelty = match visible {
1086 Some(ledger) => novelty_penalty(ledger, &e.symbol_id, changed),
1087 None => 1.0,
1088 };
1089 if novelty < 1.0 {
1090 continue;
1092 }
1093 let task_ppr = task_of.get(&e.symbol_id).copied().unwrap_or(0.0);
1094 let global_ppr = global_of.get(&e.symbol_id).copied().unwrap_or(0.0);
1095 let lexical = entry_lexical(e, &goal_terms);
1096 let criticality = if seed_ids.contains(&e.symbol_id) || required.contains(&e.id) {
1097 1.0
1098 } else {
1099 crate::surface::file_importance(&e.path) * 0.5
1103 };
1104 let change_risk = if changed { 1.0 } else { 0.0 };
1105 let semantic = match request.semantic {
1110 Some(scorer) => compiler
1111 .view
1112 .entity(&e.symbol_id)
1113 .map(|en| scorer.score(goal.unwrap_or(""), en))
1114 .unwrap_or(0.0)
1115 .clamp(0.0, 1.0),
1116 None => 0.0,
1117 };
1118 let (importance, semantic_component) = if stages.lexical {
1119 let blend = crate::pagerank::final_importance(
1126 task_ppr,
1127 global_ppr,
1128 lexical,
1129 semantic,
1130 e.confidence as f64,
1131 criticality,
1132 change_risk,
1133 0.0,
1134 has_task,
1135 );
1136 let total = match request.semantic {
1137 Some(_) => blend + crate::pagerank::NOVELTY_WEIGHT * novelty,
1139 None => blend * REDISTRIBUTION_SCALE + crate::pagerank::NOVELTY_WEIGHT * novelty,
1145 };
1146 (total, semantic)
1147 } else {
1148 (lexical, 0.0)
1150 };
1151 if importance <= 0.0 && !required.contains(&e.id) {
1152 continue;
1153 }
1154 ranks.insert(
1155 e.id.clone(),
1156 SurfaceRank {
1157 task_ppr,
1158 global_ppr,
1159 lexical,
1160 semantic: semantic_component,
1161 confidence: e.confidence as f64,
1162 criticality,
1163 change_risk,
1164 novelty,
1165 total: importance,
1166 reasons: entry_reasons(e, goal, &seed_ids, &inv_names, changed),
1167 },
1168 );
1169 ranked.push((e.id.clone(), importance));
1170 }
1171 for e in &mut map.entries {
1177 if let Some(r) = ranks.get(&e.id) {
1178 e.rank = r.clone();
1179 e.importance = Some(importance_profile(e, r));
1180 }
1181 }
1182 ranked.sort_by(|a, b| {
1183 b.1.partial_cmp(&a.1)
1184 .unwrap_or(std::cmp::Ordering::Equal)
1185 .then_with(|| a.0.cmp(&b.0))
1186 });
1187 finish_selection(
1188 &map,
1189 ranked,
1190 &required,
1191 request.budget,
1192 &request.policy,
1193 stages,
1194 request.explain,
1195 )
1196}
1197
1198pub fn select_and_render_global(
1204 compiler: &ContextCompiler,
1205 budget: usize,
1206) -> scc_core::SurfaceRenderResult {
1207 build_surface(
1208 compiler,
1209 SurfaceRequest {
1210 mode: SurfaceMode::Global,
1211 budget,
1212 explain: false,
1213 policy: SurfacePolicy::defaults(budget),
1214 semantic: None,
1215 },
1216 )
1217}
1218
1219pub fn important_symbols(
1227 compiler: &ContextCompiler,
1228 mode: SurfaceMode<'_>,
1229 limit: usize,
1230) -> Vec<SurfaceEntry> {
1231 let mut map = compile_surface_map(compiler);
1232 let ranker = crate::pagerank::SystemRanker::new(&compiler.view);
1239 let gv = ranker.global_vector();
1240 let global_of: BTreeMap<String, f64> =
1241 ranker.project_to_symbols(&gv).into_iter().collect();
1242 let task_of: BTreeMap<String, f64> = match &mode {
1243 SurfaceMode::Task { goal, .. } => {
1244 let cands =
1245 crate::rank::collect_lexical_candidates(compiler.store, &compiler.view, goal, &[], 16);
1246 let seeds: Vec<TaskSeed> = cands
1247 .iter()
1248 .map(|c| TaskSeed {
1249 kind: c.kind.clone(),
1250 id: c.id.clone(),
1251 weight: c.score,
1252 })
1253 .collect();
1254 if seeds.is_empty() {
1255 BTreeMap::new()
1256 } else {
1257 ranker
1258 .project_to_symbols(&ranker.task_vector(&seeds))
1259 .into_iter()
1260 .collect()
1261 }
1262 }
1263 SurfaceMode::Global => BTreeMap::new(),
1264 };
1265 let tasked = !task_of.is_empty();
1266 for e in &mut map.entries {
1267 let task_ppr = task_of.get(&e.symbol_id).copied().unwrap_or(0.0);
1268 let global_ppr = global_of.get(&e.symbol_id).copied().unwrap_or(0.0);
1269 let score = if tasked { task_ppr } else { global_ppr };
1270 let r = SurfaceRank {
1271 task_ppr,
1272 global_ppr,
1273 lexical: 0.0,
1274 semantic: 0.0,
1275 confidence: e.confidence as f64,
1276 criticality: 0.0,
1277 change_risk: 0.0,
1278 novelty: 1.0,
1279 total: score,
1280 reasons: Vec::new(),
1281 };
1282 e.rank = r.clone();
1283 e.importance = Some(importance_profile(e, &r));
1284 }
1285 map.entries
1286 .sort_by(|a, b| {
1287 b.rank
1288 .total
1289 .partial_cmp(&a.rank.total)
1290 .unwrap_or(std::cmp::Ordering::Equal)
1291 .then_with(|| a.id.cmp(&b.id))
1292 });
1293 if limit > 0 {
1294 map.entries.truncate(limit);
1295 }
1296 map.entries
1297}
1298
1299pub fn render_important(entries: &[SurfaceEntry], task_mode: bool) -> String {
1305 let mut out = String::new();
1306 out.push_str(if task_mode {
1307 "## TASK-CRITICAL SYMBOLS
1308"
1309 } else {
1310 "## SYSTEM-CRITICAL SYMBOLS
1311"
1312 });
1313 if entries.is_empty() {
1314 out.push_str("(no ranked symbols)
1315");
1316 return out;
1317 }
1318 for (i, e) in entries.iter().enumerate() {
1319 let imp = e.importance.as_ref();
1320 let badges = imp
1321 .map(|p| {
1322 if p.badges.is_empty() {
1323 String::new()
1324 } else {
1325 format!(" {}", p.badges.join(" \u{00B7} "))
1326 }
1327 })
1328 .unwrap_or_default();
1329 let name = &e.qualified_name;
1330 out.push_str(&format!("{}. {name}{}
1331", i + 1, badges));
1332 out.push_str(&format!(" {}:{}-{}
1333", e.path, e.range.start_line, e.range.end_line));
1334 let cc = imp.map(|p| p.caller_count).unwrap_or(e.caller_count);
1335 let ce = imp.map(|p| p.callee_count).unwrap_or(e.callee_count);
1336 out.push_str(&format!(
1337 " Called by {cc} symbols \u{00B7} calls {ce} \u{00B7} flows {} \u{00B7} contracts {} \u{00B7} importance {:.2}
1338",
1339 e.flows.len(),
1340 e.contracts.len(),
1341 e.rank.total
1342 ));
1343 }
1344 out
1345}
1346
1347pub fn select_and_render_task(
1353 compiler: &ContextCompiler,
1354 goal: &str,
1355 budget: usize,
1356 visible: &ContextLedger,
1357) -> scc_core::SurfaceRenderResult {
1358 build_surface(
1359 compiler,
1360 SurfaceRequest {
1361 mode: SurfaceMode::Task {
1362 goal,
1363 visible: Some(visible),
1364 },
1365 budget,
1366 explain: false,
1367 policy: SurfacePolicy::defaults(budget),
1368 semantic: None,
1369 },
1370 )
1371}
1372
1373#[allow(clippy::too_many_arguments)]
1378fn importance_profile(e: &SurfaceEntry, r: &SurfaceRank) -> scc_core::ImportanceProfile {
1385 let mut badges: Vec<String> = Vec::new();
1386 let entrypoint = !e.invocation_surfaces.is_empty();
1387 let dependent_count = e.caller_count;
1391 if entrypoint {
1392 badges.push("ENTRYPOINT".into());
1393 }
1394 if e.exported {
1395 badges.push("PUBLIC API".into());
1396 }
1397 if !e.state_authorities.is_empty() {
1398 badges.push("STATE OWNER".into());
1399 }
1400 if !e.contracts.is_empty() {
1401 badges.push("CONTRACT BOUNDARY".into());
1402 }
1403 if !e.flows.is_empty() {
1404 badges.push("FLOW CRITICAL".into());
1405 }
1406 if e.caller_count >= 50 {
1407 badges.push("HIGH FAN-IN".into());
1408 }
1409 if e.callee_count >= 10 {
1410 badges.push("HUB".into());
1411 }
1412 if r.global_ppr >= 0.01 {
1413 badges.push("HIGH IMPACT".into());
1414 }
1415 if r.change_risk >= 0.5 {
1416 badges.push("CHANGE HOTSPOT".into());
1417 }
1418 if r.global_ppr >= 0.005
1422 && (entrypoint || e.exported || !e.state_authorities.is_empty() || !e.contracts.is_empty() || !e.flows.is_empty())
1423 {
1424 badges.insert(0, "CORE".into());
1425 }
1426 scc_core::ImportanceProfile {
1427 overall: r.total,
1428 caller_count: e.caller_count,
1429 callee_count: e.callee_count,
1430 global_ppr: r.global_ppr,
1431 task_ppr: r.task_ppr,
1432 entrypoint,
1433 exported: e.exported,
1434 flow_count: e.flows.len(),
1435 contract_count: e.contracts.len(),
1436 state_read_count: 0,
1437 state_write_count: e.state_authorities.len(),
1438 dependent_count,
1439 change_risk: r.change_risk,
1440 badges,
1441 }
1442}
1443
1444
1445#[allow(clippy::too_many_arguments)]
1446fn build_entry(
1448 compiler: &ContextCompiler,
1449 e: &scc_core::Entity,
1450 kind_str: &str,
1451 symbol_comp_id: &BTreeMap<String, String>,
1452 comp_names: &BTreeMap<String, String>,
1453 subsys_of_comp: &BTreeMap<String, String>,
1454 surface_by_symbol: &BTreeMap<String, Vec<(String, String)>>,
1455 flows_by_actor: &HashMap<&str, Vec<&str>>,
1456 all_actors: &[&str],
1457 flows_by_actor_file: &mut HashMap<String, Vec<String>>,
1458 routes_by_handler: &HashMap<&str, Vec<(String, String)>>,
1459) -> SurfaceEntry {
1460 let view = &compiler.view;
1461 let name = e.name.clone();
1462 let simple = name.rsplit('.').next().unwrap_or(&name).to_string();
1463 let parent = attr_str(e, "parent");
1464 let exported = e.attributes.get("exported").and_then(|v| v.as_bool()) == Some(true);
1465 let file = attr_str(e, "file").unwrap_or_default();
1466 let start = attr_u32(e, "start_line");
1467 let end = attr_u32(e, "end_line");
1468
1469 let decl = attr_str(e, "decl_header").unwrap_or_default();
1474 let source_sig = if decl.trim().is_empty() {
1475 attr_str(e, "signature").unwrap_or_default()
1476 } else {
1477 decl
1478 };
1479 let source_signature = if source_sig.trim().is_empty() {
1480 synthesized_signature(kind_str, &simple)
1481 } else {
1482 source_sig
1483 };
1484 let parsed_inner = parse_sig_inner(&source_signature);
1485 let parsed_sig = parse_signature(&source_signature, &name, parent.as_deref());
1486
1487 let overload = e
1494 .attributes
1495 .get("overload_index")
1496 .and_then(|v| v.as_u64())
1497 .map(|n| n as usize);
1498 let (entry_id, symbol_id) = match overload {
1499 Some(n) => {
1500 let logical = if n >= 1 {
1501 e.id.trim_end_matches(&format!("#{n}")).to_string()
1502 } else {
1503 e.id.clone()
1504 };
1505 (format!("{}#overload{}", logical, n), logical)
1506 }
1507 None => (e.id.clone(), e.id.clone()),
1508 };
1509
1510 let kind = map_kind(kind_str, &name, parent.as_deref());
1511 let qualified = if kind_str == "method" {
1512 match &parent {
1513 Some(p) if !p.is_empty() => format!("{}.{}", p, simple),
1514 _ => name.clone(),
1515 }
1516 } else {
1517 name.clone()
1518 };
1519
1520 let visibility = entry_visibility(
1521 exported,
1522 parent.as_deref(),
1523 &parsed_inner.modifiers,
1524 parsed_inner.ret_before_name.as_deref(),
1525 );
1526
1527 const SURFACE_MODIFIERS: [&str; 5] = ["async", "static", "final", "abstract", "readonly"];
1529 let mut modifiers: Vec<String> = parsed_inner
1530 .modifiers
1531 .iter()
1532 .filter(|m| SURFACE_MODIFIERS.contains(&m.as_str()))
1533 .cloned()
1534 .collect();
1535 if parsed_sig.parameters.iter().any(|p| p.variadic)
1536 && !modifiers.iter().any(|m| m == "variadic")
1537 {
1538 modifiers.push("variadic".into());
1539 }
1540
1541 let mut annotations: Vec<String> = Vec::new();
1543 for r in sorted_rels(view.in_pred(&e.id, predicates::ANNOTATES)) {
1544 if let Some(a) = view.entity(&r.subject) {
1545 annotations.push(a.name.clone());
1546 }
1547 }
1548 annotations.sort();
1549 annotations.dedup();
1550
1551 let mut flows: BTreeSet<String> = BTreeSet::new();
1555 for key in [&e.id, &name, &qualified] {
1556 if let Some(names) = flows_by_actor.get(key.as_str()) {
1557 flows.extend(names.iter().map(|s| s.to_string()));
1558 }
1559 }
1560 if !file.is_empty() {
1561 let cached = flows_by_actor_file.entry(file.clone()).or_insert_with(|| {
1562 let mut names: Vec<String> = Vec::new();
1563 for actor in all_actors.iter() {
1564 if actor.contains(file.as_str()) {
1565 if let Some(fs) = flows_by_actor.get(*actor) {
1566 names.extend(fs.iter().map(|s| s.to_string()));
1567 }
1568 }
1569 }
1570 names.sort();
1571 names.dedup();
1572 names
1573 });
1574 flows.extend(cached.iter().cloned());
1575 }
1576
1577 let mut contracts: BTreeSet<String> = BTreeSet::new();
1579 if let Some(routes) = routes_by_handler.get(e.id.as_str()) {
1581 for (m, path) in routes {
1582 contracts.insert(format!("http: {}", format!("{m} {path}").trim()));
1583 }
1584 }
1585 if let Some(flags) = e.attributes.get("cli_flags").and_then(|v| v.as_array()) {
1587 for f in flags {
1588 if let Some(s) = f.as_str() {
1589 contracts.insert(format!("cli: {}", s));
1590 }
1591 }
1592 }
1593 for pred in [predicates::CONSUMES, predicates::PUBLISHES] {
1595 for rel in sorted_rels(view.out_pred(&e.id, pred)) {
1596 if let Some(t) = view.entity(&rel.object) {
1597 if t.kind == kinds::TOPIC {
1598 contracts.insert(format!("event: {}", t.name));
1599 }
1600 }
1601 }
1602 }
1603 for rel in sorted_rels(view.out_pred(&e.id, predicates::REGISTERS)) {
1605 if let Some(t) = view.entity(&rel.object) {
1606 if t.kind == kinds::CONTRACT {
1607 contracts.insert(format!("register:{}", view.name_of(&t.id)));
1608 }
1609 }
1610 }
1611 for rel in sorted_rels(view.out_pred(&e.id, predicates::DEFINES)) {
1613 if let Some(t) = view.entity(&rel.object) {
1614 if t.kind == kinds::SCHEMA {
1615 contracts.insert(format!("schema:{}", t.name));
1616 }
1617 }
1618 }
1619
1620 let mut state_authorities: Vec<String> = Vec::new();
1622 for rel in sorted_rels(view.out_pred(&e.id, predicates::OWNS)) {
1623 if let Some(t) = view.entity(&rel.object) {
1624 if t.kind == kinds::STATE || t.kind == kinds::REACTIVE {
1625 state_authorities.push(t.name.clone());
1626 }
1627 }
1628 }
1629 state_authorities.sort();
1630 state_authorities.dedup();
1631
1632 let mut inv: Vec<String> = Vec::new();
1634 if let Some(surfs) = surface_by_symbol.get(&e.id) {
1635 for (kind, trigger) in surfs {
1636 inv.push(format!("{}: {}", kind, trigger));
1637 }
1638 }
1639 if let Some(eps) = e.attributes.get("entrypoints").and_then(|v| v.as_array()) {
1640 for ep in eps {
1641 if let Some(s) = ep.as_str() {
1642 inv.push(format!("entrypoint:{}", s));
1643 }
1644 }
1645 }
1646 inv.sort();
1647
1648 let mut all_callers: Vec<String> = Vec::new();
1651 for r in sorted_rels(view.in_pred(&e.id, predicates::CALLS)) {
1652 all_callers.push(view.name_of(&r.subject));
1653 }
1654 all_callers.sort();
1655 all_callers.dedup();
1656 let caller_count = all_callers.len();
1657 let mut callers = all_callers;
1658 callers.truncate(12);
1659 let mut all_callees: Vec<String> = Vec::new();
1660 for r in sorted_rels(view.out_pred(&e.id, predicates::CALLS)) {
1661 all_callees.push(view.name_of(&r.object));
1662 }
1663 all_callees.sort();
1664 all_callees.dedup();
1665 let callee_count = all_callees.len();
1666 let mut callees = all_callees;
1667 callees.truncate(12);
1668
1669 let has_resolved_call = view
1671 .out_pred(&e.id, predicates::CALLS)
1672 .iter()
1673 .any(|r| r.provenance == Provenance::Resolved);
1674 let provenance = if has_resolved_call {
1675 Provenance::Resolved
1676 } else {
1677 Provenance::Extracted
1678 };
1679 let confidence: f32 = if has_resolved_call { 1.0 } else { 0.85 };
1680
1681 let component = symbol_comp_id
1683 .get(&e.id)
1684 .and_then(|cid| comp_names.get(cid))
1685 .cloned();
1686 let subsystem = symbol_comp_id
1687 .get(&e.id)
1688 .and_then(|cid| subsys_of_comp.get(cid))
1689 .cloned();
1690
1691 SurfaceEntry {
1692 id: entry_id,
1693 symbol_id,
1694 qualified_name: qualified,
1695 kind,
1696 path: file.clone(),
1697 range: SourceRange::new(file, start, end),
1698 source_signature: source_signature.clone(),
1699 canonical_signature: canonicalize(&source_signature),
1700 semantic_signature: parsed_sig,
1701 visibility,
1702 exported,
1703 modifiers,
1704 annotations,
1705 component,
1706 subsystem,
1707 flows: flows.into_iter().collect(),
1708 contracts: contracts.into_iter().collect(),
1709 state_authorities,
1710 invocation_surfaces: inv,
1711 callers,
1712 callees,
1713 caller_count,
1714 callee_count,
1715 importance: None,
1716 provenance,
1717 confidence,
1718 rank: SurfaceRank::default(),
1719 }
1720}
1721
1722#[derive(Debug, Default)]
1727struct ParsedSignature {
1729 modifiers: Vec<String>,
1730 async_: bool,
1731 name: String,
1732 owner: Option<String>,
1733 ret_before_name: Option<String>,
1734 params: Vec<String>,
1735 generic_parameters: Vec<String>,
1736 returns: Option<String>,
1737 constraints: Vec<String>,
1738}
1739
1740fn parse_signature(sig: &str, fallback_name: &str, parent: Option<&str>) -> SemanticSignature {
1742 let p = parse_sig_inner(sig);
1743 let name = if p.name.is_empty() {
1744 fallback_name.to_string()
1745 } else {
1746 p.name
1747 };
1748 let mut parameters: Vec<SemanticParameter> = Vec::new();
1749 for raw in &p.params {
1750 if let Some(sp) = parse_param(raw) {
1751 parameters.push(sp);
1752 }
1753 }
1754 let mut generic_parameters = p.generic_parameters;
1755 generic_parameters.sort();
1756 generic_parameters.dedup();
1757 SemanticSignature {
1758 name,
1759 owner: p.owner.or_else(|| parent.map(|s| s.to_string())),
1760 visibility: visibility_from_modifiers(&p.modifiers),
1761 async_: p.async_,
1762 generic_parameters,
1763 parameters,
1764 returns: p.returns,
1765 constraints: p.constraints,
1766 }
1767}
1768
1769fn parse_sig_inner(sig: &str) -> ParsedSignature {
1771 let mut p = ParsedSignature::default();
1772 let s = sig.trim();
1773 if s.is_empty() {
1774 return p;
1775 }
1776
1777 let mut rest: &str = s;
1779 let mut consumed_keyword: Option<String> = None;
1780 while let Some((word, after)) = leading_word(rest) {
1781 let is_mod = is_modifier_word(&word);
1782 let is_kw = !is_mod && is_callable_keyword(&word);
1783 if !is_mod && !is_kw {
1784 break;
1785 }
1786 let mut after = after;
1787 let token = if after.starts_with('(') {
1788 match take_group(after, '(', ')') {
1789 Some((g, r)) => {
1790 after = r;
1791 format!("{}({})", word, g)
1792 }
1793 None => word.clone(),
1794 }
1795 } else {
1796 word.clone()
1797 };
1798 if is_mod {
1799 if word == "async" {
1800 p.async_ = true;
1801 }
1802 if !p.modifiers.iter().any(|m| m == &token) {
1803 p.modifiers.push(token);
1804 }
1805 } else {
1806 consumed_keyword = Some(word);
1807 }
1808 rest = after.trim_start();
1809 }
1810
1811 let is_go = matches!(consumed_keyword.as_deref(), Some("func") | Some("function"));
1813 if is_go && rest.starts_with('(') {
1814 if let Some((group, after)) = take_group(rest, '(', ')') {
1815 p.owner = receiver_owner(&group);
1816 rest = after.trim_start();
1817 }
1818 }
1819
1820 let (prefix, params, tail) = match split_params(rest) {
1822 Some(x) => (x.0, x.1, x.2),
1823 None => {
1824 if let Some((w, _)) = leading_word(rest) {
1825 if is_identifier(&w) {
1826 p.name = w;
1827 }
1828 }
1829 return p;
1830 }
1831 };
1832 p.params = params;
1833
1834 let prefix = prefix.trim();
1836 let (name_region, generics) = if prefix.ends_with('>') {
1837 match find_matching_open(prefix, '<', '>') {
1838 Some(idx) => (&prefix[..idx], Some(&prefix[idx..])),
1839 None => (prefix, None),
1840 }
1841 } else {
1842 (prefix, None)
1843 };
1844 let words: Vec<&str> = name_region.split_whitespace().collect();
1845 if let Some(last) = words.last() {
1846 if is_identifier(last) {
1847 p.name = last.to_string();
1848 if words.len() > 1 {
1849 p.ret_before_name = Some(words[..words.len() - 1].join(" "));
1850 }
1851 } else if let Some(first) = words.first() {
1852 if is_identifier(first) {
1853 p.name = first.to_string();
1854 }
1855 }
1856 }
1857
1858 if let Some(g) = generics {
1859 let inner = g.trim_start_matches('<').trim_end_matches('>');
1860 let mut seen: BTreeSet<String> = BTreeSet::new();
1861 for item in split_top(inner, ',') {
1862 let item = item.trim();
1863 if item.is_empty() {
1864 continue;
1865 }
1866 let (name_part, bound) = match item.find(':') {
1867 Some(idx) => (&item[..idx], Some(item[idx + 1..].trim())),
1868 None => (item, None),
1869 };
1870 let gn = name_part.trim();
1871 if gn.is_empty() {
1872 continue;
1873 }
1874 if seen.insert(gn.to_string()) {
1875 p.generic_parameters.push(gn.to_string());
1876 }
1877 if let Some(b) = bound {
1878 let c = format!("{}: {}", gn, b);
1879 if !c.is_empty() && seen.insert(c.clone()) {
1880 p.constraints.push(c);
1881 }
1882 }
1883 }
1884 }
1885
1886 let (ret_text, constraints) = split_tail(tail.trim());
1888 p.returns = parse_return(&ret_text, p.ret_before_name.as_deref());
1889 if let Some(cs) = constraints {
1890 for c in split_top(&cs, ',') {
1891 let c = c.trim();
1892 if !c.is_empty() && !p.constraints.iter().any(|x| x == c) {
1893 p.constraints.push(c.to_string());
1894 }
1895 }
1896 }
1897 p
1898}
1899
1900fn split_tail(tail: &str) -> (String, Option<String>) {
1902 let t = tail.trim();
1903 for marker in ["where ", "throws ", " where ", " throws "] {
1904 if let Some(idx) = find_depth0_pattern(t, marker) {
1905 let before = if idx == 0 {
1906 String::new()
1907 } else {
1908 t[..idx].trim().to_string()
1909 };
1910 let after = t[idx + marker.len()..]
1911 .trim()
1912 .trim_end_matches(',')
1913 .trim()
1914 .to_string();
1915 return (before, Some(after));
1916 }
1917 }
1918 (t.to_string(), None)
1919}
1920
1921fn parse_return(text: &str, ret_before: Option<&str>) -> Option<String> {
1923 let t = text
1924 .trim()
1925 .trim_start_matches(':')
1926 .trim()
1927 .trim_end_matches(';')
1928 .trim()
1929 .to_string();
1930 if t.is_empty() {
1931 return ret_before.map(|s| s.to_string());
1932 }
1933 for arrow in ["->", "=>"] {
1934 if let Some(idx) = find_depth0_pattern(&t, arrow) {
1935 let mut r = t[idx + arrow.len()..]
1936 .trim()
1937 .trim_end_matches(':')
1938 .trim()
1939 .trim_end_matches(';')
1940 .trim()
1941 .to_string();
1942 if let Some((g, _)) = take_group(&r, '(', ')') {
1944 r = g;
1945 }
1946 if !r.is_empty() {
1947 while let Some(stripped) = r.strip_prefix('*').or_else(|| r.strip_prefix('&')) {
1949 r = stripped.trim().to_string();
1950 }
1951 return Some(r);
1952 }
1953 return ret_before.map(|s| s.to_string());
1954 }
1955 }
1956 if t.starts_with('(') {
1957 if let Some((g, _)) = take_group(&t, '(', ')') {
1958 return Some(g);
1959 }
1960 }
1961 if let Some(rb) = ret_before {
1962 return Some(rb.to_string());
1963 }
1964 if t.chars().all(|c| c.is_whitespace() || matches!(c, ':' | ';' | ',')) {
1965 return None;
1966 }
1967 let mut r = t;
1969 while let Some(stripped) = r.strip_prefix('*').or_else(|| r.strip_prefix('&')) {
1970 r = stripped.trim().to_string();
1971 }
1972 if r.is_empty() { None } else { Some(r) }
1973}
1974
1975fn parse_param(raw: &str) -> Option<SemanticParameter> {
1977 let mut s = raw.trim().to_string();
1978 if s.is_empty() {
1979 return None;
1980 }
1981 let variadic = s.contains("...") || s.starts_with('*') || s.starts_with("**");
1982
1983 for pfx in ["&mut ", "&", "*", "mut ", "ref ", "..."] {
1984 if let Some(after) = s.strip_prefix(pfx) {
1985 s = after.trim().to_string();
1986 break;
1987 }
1988 }
1989 let head = leading_word(&s).map(|(w, _)| w).unwrap_or_default();
1990 if head == "self" || head == "this" || head == "Self" {
1991 return Some(SemanticParameter {
1992 name: head,
1993 ty: None,
1994 receiver: true,
1995 default: None,
1996 variadic,
1997 });
1998 }
1999
2000 let (left, default) = match find_depth0_char(&s, '=') {
2001 Some(idx) => (s[..idx].trim().to_string(), Some(s[idx + 1..].trim().to_string())),
2002 None => (s, None),
2003 };
2004 if left.is_empty() {
2005 return None;
2006 }
2007 let (mut name, ty) = if let Some(idx) = find_depth0_char(&left, ':') {
2008 (left[..idx].trim().to_string(), Some(left[idx + 1..].trim().to_string()))
2009 } else {
2010 split_name_type(&left)
2011 };
2012 while let Some(stripped) = name.strip_prefix('&').or_else(|| name.strip_prefix('*')) {
2013 name = stripped.trim().to_string();
2014 }
2015 if let Some(stripped) = name.strip_prefix("mut ") {
2016 name = stripped.trim().to_string();
2017 }
2018 name = name.trim_end_matches('?').trim().to_string();
2019 if name.is_empty() {
2020 return None;
2021 }
2022 let receiver = name == "self" || name == "this";
2023 Some(SemanticParameter {
2024 name,
2025 ty,
2026 receiver,
2027 default,
2028 variadic,
2029 })
2030}
2031
2032fn split_name_type(left: &str) -> (String, Option<String>) {
2034 let words: Vec<&str> = left.split_whitespace().collect();
2035 if words.is_empty() {
2036 return (String::new(), None);
2037 }
2038 if words.len() == 1 {
2039 return (words[0].to_string(), None);
2040 }
2041 let first = words[0];
2042 let last = words[words.len() - 1];
2043 let first_is_type = is_type_word(first)
2044 || first.chars().next().map(|c| !c.is_ascii_lowercase()).unwrap_or(false)
2045 || first.contains('<')
2046 || first.contains('[')
2047 || first.contains('.')
2048 || first.ends_with("[]");
2049 let last_is_plain = is_identifier(last) && !is_type_word(last);
2050 if first_is_type && last_is_plain {
2051 (last.to_string(), Some(words[..words.len() - 1].join(" ")))
2052 } else {
2053 (first.to_string(), Some(words[1..].join(" ")))
2054 }
2055}
2056
2057fn visibility_from_modifiers(mods: &[String]) -> Option<Visibility> {
2063 if mods.iter().any(|m| m == "pub" || m == "public") {
2064 Some(Visibility::Public)
2065 } else if mods.iter().any(|m| m == "protected") {
2066 Some(Visibility::Protected)
2067 } else if mods.iter().any(|m| m == "private") {
2068 Some(Visibility::Private)
2069 } else {
2070 None
2071 }
2072}
2073
2074fn entry_visibility(
2076 exported: bool,
2077 parent: Option<&str>,
2078 mods: &[String],
2079 ret_before_name: Option<&str>,
2080) -> Visibility {
2081 if exported {
2082 return Visibility::Public;
2083 }
2084 if let Some(v) = visibility_from_modifiers(mods) {
2085 return v;
2086 }
2087 if parent.is_some() && ret_before_name.is_some() {
2088 return Visibility::Package;
2089 }
2090 Visibility::Private
2091}
2092
2093fn map_kind(kind_str: &str, name: &str, parent: Option<&str>) -> SurfaceKind {
2099 match kind_str {
2100 "method" => {
2101 let simple = name.rsplit('.').next().unwrap_or(name);
2102 if parent.map(|p| simple == p).unwrap_or(false) || simple == "__init__" {
2103 SurfaceKind::Constructor
2104 } else {
2105 SurfaceKind::Method
2106 }
2107 }
2108 "function" => SurfaceKind::Function,
2109 "class" => SurfaceKind::Class,
2110 "interface" => SurfaceKind::Interface,
2111 "trait" => SurfaceKind::Trait,
2112 "enum" => SurfaceKind::Enum,
2113 "type" => SurfaceKind::Type,
2114 "const" => SurfaceKind::Const,
2115 "module" => SurfaceKind::Module,
2116 _ => SurfaceKind::Function,
2117 }
2118}
2119
2120fn canonicalize(sig: &str) -> String {
2126 sig.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
2127}
2128
2129fn synthesized_signature(kind_str: &str, simple: &str) -> String {
2131 match kind_str {
2132 "class" | "interface" | "trait" | "enum" | "module" => format!("{} {}", kind_str, simple),
2133 "type" => format!("type {}", simple),
2134 _ => simple.to_string(),
2135 }
2136}
2137
2138fn sorted_rels(rels: Vec<&scc_core::Relationship>) -> Vec<&scc_core::Relationship> {
2140 let mut v = rels;
2141 v.sort_by(|a, b| {
2142 a.id.cmp(&b.id)
2143 .then_with(|| a.subject.cmp(&b.subject))
2144 .then_with(|| a.object.cmp(&b.object))
2145 });
2146 v
2147}
2148
2149fn attr_str(e: &scc_core::Entity, key: &str) -> Option<String> {
2151 e.attributes.get(key).and_then(|v| match v {
2152 serde_json::Value::String(s) => Some(s.clone()),
2153 _ => None,
2154 })
2155}
2156
2157fn attr_u32(e: &scc_core::Entity, key: &str) -> u32 {
2159 e.attributes
2160 .get(key)
2161 .and_then(|v| v.as_u64())
2162 .map(|n| n.min(u32::MAX as u64) as u32)
2163 .unwrap_or(0)
2164}
2165
2166fn fits(total_chars: usize, budget_chars: Option<usize>) -> bool {
2168 budget_chars.is_none_or(|b| total_chars <= b)
2169}
2170
2171fn entry_tier(e: &SurfaceEntry) -> u8 {
2173 if e.exported || e.visibility == Visibility::Public || !e.invocation_surfaces.is_empty() {
2174 0
2175 } else if e.rank.total > 0.0 {
2176 1
2177 } else {
2178 2
2179 }
2180}
2181
2182fn entry_order(a: &SurfaceEntry, b: &SurfaceEntry) -> std::cmp::Ordering {
2184 entry_tier(a)
2185 .cmp(&entry_tier(b))
2186 .then_with(|| b.rank.total.partial_cmp(&a.rank.total).unwrap_or(std::cmp::Ordering::Equal))
2187 .then_with(|| a.kind.as_str().cmp(b.kind.as_str()))
2188 .then_with(|| a.qualified_name.cmp(&b.qualified_name))
2189 .then_with(|| a.id.cmp(&b.id))
2190}
2191
2192fn render_entry(e: &SurfaceEntry) -> String {
2198 render_entry_opt(e, 0, None)
2199}
2200
2201fn render_entry_compressed(e: &SurfaceEntry) -> String {
2209 render_entry_opt(e, 1, None)
2210}
2211
2212fn render_entry_opt(e: &SurfaceEntry, level: u8, rank: Option<&SurfaceRank>) -> String {
2221 let mut out = String::new();
2222 let name = e.qualified_name.rsplit('.').next().unwrap_or(&e.qualified_name);
2223 out.push_str(&format!(" {} {}\n\n", e.kind.as_str(), name));
2224 if (1..3).contains(&level) {
2225 let sig = match level {
2230 2 => e.canonical_signature.split('\n').next().unwrap_or(""),
2231 _ => e.source_signature.split('\n').next().unwrap_or(""),
2232 };
2233 if !sig.is_empty() {
2234 let sig: String = sig.chars().take(160).collect();
2235 out.push_str(" ");
2236 out.push_str(&sig);
2237 out.push('\n');
2238 }
2239 } else if level == 0 {
2240 let sig_lines: Vec<&str> = e.source_signature.split('\n').collect();
2241 for line in &sig_lines {
2242 out.push_str(" ");
2243 out.push_str(line);
2244 out.push('\n');
2245 }
2246 }
2247 if level == 0 {
2248 let sections: [(&str, &[String]); 6] = [
2249 ("Used by", &e.callers),
2250 ("Calls", &e.callees),
2251 ("Participates in", &e.flows),
2252 ("Contracts", &e.contracts),
2253 ("Owns", &e.state_authorities),
2254 ("Invocation", &e.invocation_surfaces),
2255 ];
2256 for (label, vals) in sections {
2257 if vals.is_empty() {
2258 continue;
2259 }
2260 out.push('\n');
2261 out.push_str(&format!(" {label}:\n {}\n", vals.join(", ")));
2262 }
2263 if e.caller_count > e.callers.len() || e.callee_count > e.callees.len() {
2266 out.push('\n');
2267 if e.caller_count > e.callers.len() {
2268 out.push_str(&format!(" Called by {} symbols (showing {})\n", e.caller_count, e.callers.len()));
2269 }
2270 if e.callee_count > e.callees.len() {
2271 out.push_str(&format!(" Calls {} symbols (showing {})\n", e.callee_count, e.callees.len()));
2272 }
2273 }
2274 if let Some(imp) = &e.importance {
2275 if !imp.badges.is_empty() {
2276 out.push('\n');
2277 out.push_str(&format!(" Badges: {}\n", imp.badges.join(" \u{00B7} ")));
2278 }
2279 }
2280 }
2281 if let Some(rank) = rank {
2282 out.push('\n');
2283 out.push_str(&format!(" importance: {:.3}\n", rank.total));
2284 out.push_str(&format!(" task_ppr: {:.3}\n", rank.task_ppr));
2285 out.push_str(&format!(" global_ppr: {:.3}\n", rank.global_ppr));
2286 out.push_str(&format!(" lexical: {:.3}\n", rank.lexical));
2287 out.push_str(&format!(" semantic: {:.3}\n", rank.semantic));
2288 out.push_str(&format!(" confidence: {:.3}\n", rank.confidence));
2289 out.push_str(&format!(" criticality: {:.3}\n", rank.criticality));
2290 out.push_str(&format!(" change_risk: {:.3}\n", rank.change_risk));
2291 out.push_str(&format!(" novelty: {:.3}\n", rank.novelty));
2292 if !rank.reasons.is_empty() {
2293 out.push_str(" because:\n");
2294 for r in &rank.reasons {
2295 out.push_str(&format!(" {r}\n"));
2296 }
2297 }
2298 }
2299 out.push('\n');
2300 out
2301}
2302
2303fn group_header(comp: &str, sub: &str, path: &str) -> String {
2305 let mut h = String::new();
2306 h.push('\n');
2307 h.push_str(&comp.to_uppercase());
2308 h.push_str("\n\n");
2309 if sub.is_empty() {
2310 h.push_str(path);
2311 } else {
2312 h.push_str(&format!("{} [{}]", path, sub));
2313 }
2314 h.push_str("\n\n");
2315 h
2316}
2317
2318fn is_modifier_word(w: &str) -> bool {
2324 matches!(
2325 w,
2326 "async" | "await" | "static" | "final" | "abstract" | "readonly"
2327 | "sealed" | "override" | "virtual" | "synchronized" | "native"
2328 | "extern" | "unsafe" | "inline" | "const" | "var" | "let" | "mutable"
2329 | "pub" | "public" | "private" | "protected" | "package" | "internal"
2330 | "open" | "suspend" | "operator" | "export" | "default" | "declare"
2331 | "data" | "value"
2332 )
2333}
2334
2335fn is_callable_keyword(w: &str) -> bool {
2337 matches!(
2338 w,
2339 "fn" | "def" | "func" | "function" | "class" | "struct" | "interface"
2340 | "trait" | "enum" | "type" | "module"
2341 )
2342}
2343
2344fn is_type_word(w: &str) -> bool {
2346 matches!(
2347 w,
2348 "int" | "long" | "short" | "byte" | "char" | "float" | "double"
2349 | "bool" | "boolean" | "string" | "str" | "void" | "unsigned"
2350 | "signed" | "usize" | "isize" | "u8" | "u16" | "u32" | "u64"
2351 | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" | "any"
2352 | "object" | "Option" | "Result" | "Vec" | "Map" | "List" | "Set"
2353 )
2354}
2355
2356fn is_identifier(w: &str) -> bool {
2358 let mut chars = w.chars();
2359 match chars.next() {
2360 Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
2361 _ => return false,
2362 }
2363 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$' || c == '?')
2364}
2365
2366fn leading_word(s: &str) -> Option<(String, &str)> {
2368 let s = s.trim_start();
2369 let mut end = 0;
2370 for (i, ch) in s.char_indices() {
2371 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '$' || ch == '?' {
2372 end = i + ch.len_utf8();
2373 } else {
2374 break;
2375 }
2376 }
2377 if end == 0 {
2378 return None;
2379 }
2380 Some((s[..end].to_string(), &s[end..]))
2381}
2382
2383fn take_group(text: &str, open: char, close: char) -> Option<(String, &str)> {
2385 if !text.starts_with(open) {
2386 return None;
2387 }
2388 let mut depth = 0i32;
2389 let mut quote: Option<char> = None;
2390 for (i, ch) in text.char_indices() {
2391 if let Some(q) = quote {
2392 if ch == q {
2393 quote = None;
2394 }
2395 continue;
2396 }
2397 match ch {
2398 '"' | '\'' | '`' => quote = Some(ch),
2399 c if c == open => depth += 1,
2400 c if c == close => {
2401 depth -= 1;
2402 if depth == 0 {
2403 return Some((text[1..i].to_string(), &text[i + ch.len_utf8()..]));
2404 }
2405 }
2406 _ => {}
2407 }
2408 }
2409 None
2410}
2411
2412fn find_matching_open(text: &str, open: char, close: char) -> Option<usize> {
2414 let mut depth = 0i32;
2415 for (i, ch) in text.char_indices().rev() {
2416 if ch == close {
2417 depth += 1;
2418 } else if ch == open {
2419 depth -= 1;
2420 if depth == 0 {
2421 return Some(i);
2422 }
2423 }
2424 }
2425 None
2426}
2427
2428fn split_params(rest: &str) -> Option<(String, Vec<String>, String)> {
2430 let mut depth = 0i32;
2431 let mut quote: Option<char> = None;
2432 let mut open: Option<usize> = None;
2433 for (i, ch) in rest.char_indices() {
2434 if let Some(q) = quote {
2435 if ch == q {
2436 quote = None;
2437 }
2438 continue;
2439 }
2440 match ch {
2441 '"' | '\'' | '`' => quote = Some(ch),
2442 '(' if depth == 0 => {
2443 open = Some(i);
2444 break;
2445 }
2446 ')' if depth == 0 => return None,
2447 '(' => depth += 1,
2448 ')' => depth -= 1,
2449 _ => {}
2450 }
2451 }
2452 let i0 = open?;
2453 let (inner, after) = take_group(&rest[i0..], '(', ')')?;
2454 let prefix = rest[..i0].to_string();
2455 let params = split_top(&inner, ',');
2456 Some((prefix, params, after.trim().to_string()))
2457}
2458
2459fn split_top(text: &str, sep: char) -> Vec<String> {
2461 let mut out: Vec<String> = Vec::new();
2462 let mut paren_depth = 0i32;
2463 let mut angle_depth = 0i32;
2464 let mut quote: Option<char> = None;
2465 let mut prev: Option<char> = None;
2466 let mut cur = String::new();
2467 for ch in text.chars() {
2468 if let Some(q) = quote {
2469 cur.push(ch);
2470 if ch == q {
2471 quote = None;
2472 }
2473 prev = Some(ch);
2474 continue;
2475 }
2476 match ch {
2477 '"' | '\'' | '`' => {
2478 quote = Some(ch);
2479 cur.push(ch);
2480 }
2481 '(' | '[' | '{' => {
2482 paren_depth += 1;
2483 cur.push(ch);
2484 }
2485 ')' | ']' | '}' => {
2486 paren_depth -= 1;
2487 cur.push(ch);
2488 }
2489 '<' if angle_depth == 0 && prev.map(|p| p.is_alphanumeric() || p == '_' || p == '>').unwrap_or(false) => {
2490 angle_depth += 1;
2491 cur.push(ch);
2492 }
2493 '>' if angle_depth > 0 && prev != Some('-') => {
2494 angle_depth -= 1;
2495 cur.push(ch);
2496 }
2497 c if c == sep && paren_depth == 0 && angle_depth == 0 => {
2498 out.push(std::mem::take(&mut cur));
2499 }
2500 _ => cur.push(ch),
2501 }
2502 prev = Some(ch);
2503 }
2504 out.push(cur);
2505 out.into_iter().map(|s| s.trim().to_string()).collect()
2506}
2507
2508fn find_depth0_pattern(text: &str, pat: &str) -> Option<usize> {
2510 let mut depth = 0i32;
2511 let mut quote: Option<char> = None;
2512 for (i, ch) in text.char_indices() {
2513 if let Some(q) = quote {
2514 if ch == q {
2515 quote = None;
2516 }
2517 continue;
2518 }
2519 match ch {
2520 '"' | '\'' | '`' => quote = Some(ch),
2521 '(' | '[' | '{' => depth += 1,
2522 ')' | ']' | '}' => depth -= 1,
2523 _ => {}
2524 }
2525 if depth == 0 && text[i..].starts_with(pat) {
2526 return Some(i);
2527 }
2528 }
2529 None
2530}
2531
2532fn find_depth0_char(text: &str, target: char) -> Option<usize> {
2534 let mut paren_depth = 0i32;
2535 let mut angle_depth = 0i32;
2536 let mut quote: Option<char> = None;
2537 let mut prev: Option<char> = None;
2538 for (i, ch) in text.char_indices() {
2539 if let Some(q) = quote {
2540 if ch == q {
2541 quote = None;
2542 }
2543 prev = Some(ch);
2544 continue;
2545 }
2546 match ch {
2547 '"' | '\'' | '`' => quote = Some(ch),
2548 '(' | '[' | '{' => paren_depth += 1,
2549 ')' | ']' | '}' => paren_depth -= 1,
2550 '<' if angle_depth == 0 && prev.map(|p| p.is_alphanumeric() || p == '_' || p == '>').unwrap_or(false) => angle_depth += 1,
2551 '>' if angle_depth > 0 && prev != Some('-') => angle_depth -= 1,
2552 c if c == target && paren_depth == 0 && angle_depth == 0 => return Some(i),
2553 _ => {}
2554 }
2555 prev = Some(ch);
2556 }
2557 None
2558}
2559
2560fn receiver_owner(group: &str) -> Option<String> {
2562 let inner = group.trim();
2563 if inner.is_empty() {
2564 return None;
2565 }
2566 let words: Vec<&str> = inner.split_whitespace().collect();
2567 if words.is_empty() {
2568 return None;
2569 }
2570 let t = if words.len() <= 1 {
2571 words[0]
2572 } else {
2573 words[words.len() - 1]
2574 };
2575 let t = t.trim_start_matches('*').trim_start_matches('&');
2576 let seg = t.rsplit('.').next().unwrap_or(t);
2577 let seg = seg.trim().trim_start_matches('*');
2578 if seg.is_empty() {
2579 None
2580 } else {
2581 Some(seg.to_string())
2582 }
2583}
2584
2585#[cfg(test)]
2590mod tests {
2591 use super::*;
2592 use scc_core::{
2593 entity_id, relationship_id, symbol_id, ContextLedger, Entity, Flow, FlowKind,
2594 FlowStep, Relationship,
2595 };
2596 use scc_store::Store;
2597
2598fn fixture_store() -> (tempfile::TempDir, Store) {
2600 let dir = tempfile::TempDir::new().unwrap();
2601 let root = dir.path().join("repo");
2602 std::fs::create_dir_all(&root).unwrap();
2603 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2604 let repo = store.repo_id.clone();
2605 let path = "api/app.py";
2606
2607 let fid = entity_id(&repo, kinds::FILE, path);
2608 store.insert_entity(&Entity::new(fid.clone(), kinds::FILE, path), &[path.to_string()]).unwrap();
2609
2610 let comp_id = entity_id(&repo, kinds::COMPONENT, "api");
2611 store.replace_components(&[Entity::new(comp_id.clone(), kinds::COMPONENT, "api")]).unwrap();
2612 store.insert_relationship(
2613 &Relationship::new(relationship_id(1), comp_id.clone(), predicates::CONTAINS, fid.clone(), Provenance::Extracted),
2614 path,
2615 ).unwrap();
2616
2617 let svc_id = entity_id(&repo, kinds::SERVICE, "core");
2618 store.insert_entity(&Entity::new(svc_id.clone(), kinds::SERVICE, "core"), &[path.to_string()]).unwrap();
2619 store.insert_relationship(
2620 &Relationship::new(relationship_id(2), svc_id, predicates::CONTAINS, comp_id.clone(), Provenance::Extracted),
2621 path,
2622 ).unwrap();
2623
2624 let mut rid: u64 = 3;
2625 let mut sym = |name: &str, kind: &str, sig: Option<&str>, exported: bool, parent: Option<&str>| -> String {
2626 let id = symbol_id(&repo, path, name);
2627 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name);
2628 e.attr("kind", serde_json::json!(kind));
2629 e.attr("file", serde_json::json!(path));
2630 if let Some(s) = sig { e.attr("signature", serde_json::json!(s)); }
2631 e.attr("exported", serde_json::json!(exported));
2632 e.attr("start_line", serde_json::json!(1u32));
2633 e.attr("end_line", serde_json::json!(10u32));
2634 if let Some(p) = parent { e.attr("parent", serde_json::json!(p)); }
2635 store.insert_entity(&e, &[path.to_string()]).unwrap();
2636 rid += 1;
2637 store.insert_relationship(
2638 &Relationship::new(relationship_id(rid), fid.clone(), predicates::CONTAINS, id.clone(), Provenance::Extracted),
2639 path,
2640 ).unwrap();
2641 id
2642 };
2643
2644 let _ = sym("UserService", "class", None, true, None);
2645 let _ = sym("UserService.UserService", "method", Some("public UserService(String name)"), false, Some("UserService"));
2646 let get_id = sym("UserService.get", "method", Some("public User get(String id) throws NotFound"), false, Some("UserService"));
2647 let _ = sym("UserService.hash", "method", Some("String hash()"), false, Some("UserService"));
2648 let update_id = sym("UserService.update", "method", Some("async fn update(&mut self, patch: Json) -> bool"), false, Some("UserService"));
2649 let create_id = sym("create_user", "function", Some("def create_user(name: str, age: int = 0) -> User"), true, None);
2650 let db_id = sym("db", "function", Some("func db() *DB"), false, None);
2651
2652 let ann_id = entity_id(&repo, kinds::ANNOTATION, "RestController");
2654 store.insert_entity(&Entity::new(ann_id.clone(), kinds::ANNOTATION, "RestController"), &[path.to_string()]).unwrap();
2655 rid += 1;
2656 store.insert_relationship(
2657 &Relationship::new(relationship_id(rid), ann_id, predicates::ANNOTATES, get_id.clone(), Provenance::Extracted),
2658 path,
2659 ).unwrap();
2660
2661 let route_id = entity_id(&repo, kinds::ROUTE, "GET /api/users");
2663 let mut re = Entity::new(route_id.clone(), kinds::ROUTE, "GET /api/users");
2664 re.attr("method", serde_json::json!("GET"));
2665 re.attr("path", serde_json::json!("/api/users"));
2666 re.attr("handler", serde_json::json!(create_id.clone()));
2667 store.insert_entity(&re, &[path.to_string()]).unwrap();
2668
2669 rid += 1;
2671 store.insert_relationship(
2672 &Relationship::new(relationship_id(rid), create_id.clone(), predicates::CALLS, get_id.clone(), Provenance::Extracted),
2673 path,
2674 ).unwrap();
2675 rid += 1;
2676 store.insert_relationship(
2677 &Relationship::new(relationship_id(rid), db_id.clone(), predicates::CALLS, create_id.clone(), Provenance::Resolved),
2678 path,
2679 ).unwrap();
2680
2681 let state_id = entity_id(&repo, kinds::STATE, "sessions");
2683 store.insert_entity(&Entity::new(state_id.clone(), kinds::STATE, "sessions"), &[path.to_string()]).unwrap();
2684 rid += 1;
2685 store.insert_relationship(
2686 &Relationship::new(relationship_id(rid), db_id.clone(), predicates::OWNS, state_id, Provenance::Extracted),
2687 path,
2688 ).unwrap();
2689
2690 let rx_id = entity_id(&repo, kinds::REACTIVE, "cursor");
2692 store.insert_entity(&Entity::new(rx_id.clone(), kinds::REACTIVE, "cursor"), &[path.to_string()]).unwrap();
2693 rid += 1;
2694 store.insert_relationship(
2695 &Relationship::new(relationship_id(rid), update_id.clone(), predicates::OWNS, rx_id, Provenance::Extracted),
2696 path,
2697 ).unwrap();
2698
2699 store.replace_flows(&[Flow {
2701 id: entity_id(&repo, kinds::FLOW, "signup"),
2702 kind: FlowKind::Workflow,
2703 name: "signup".into(),
2704 trigger: Some("http".into()),
2705 steps: vec![
2706 FlowStep {
2707 id: "step:1".into(),
2708 order: 1,
2709 actor: get_id.clone(),
2710 operation: "load user".into(),
2711 condition: None,
2712 r#async: None,
2713 timeout_ms: None,
2714 retry_policy: None,
2715 failure_outcome: None,
2716 provenance: Some(Provenance::Extracted),
2717 evidence: vec![],
2718 },
2719 FlowStep {
2720 id: "step:2".into(),
2721 order: 2,
2722 actor: "db".into(),
2723 operation: "persist".into(),
2724 condition: None,
2725 r#async: None,
2726 timeout_ms: None,
2727 retry_policy: None,
2728 failure_outcome: None,
2729 provenance: Some(Provenance::Extracted),
2730 evidence: vec![],
2731 },
2732 ],
2733 attributes: std::collections::BTreeMap::new(),
2734 }]).unwrap();
2735
2736 let _ = (get_id, create_id, db_id, update_id);
2737 (dir, store)
2738 }
2739
2740fn entry<'a>(map: &'a SystemSurfaceMap, qn: &str) -> &'a SurfaceEntry {
2742 map.entries.iter().find(|e| e.qualified_name == qn).expect("entry not found")
2743 }
2744
2745fn make_ctx<'a>(store: &'a Store) -> ContextCompiler<'a> {
2747 let graph = Box::leak(Box::new(scc_graph::RealityGraph::load(store).unwrap()));
2748 ContextCompiler::new(store, graph, crate::ContextSettings::default(), Vec::new())
2749 }
2750
2751 fn zeta_alpha_fixture() -> (tempfile::TempDir, Store) {
2756 let dir = tempfile::TempDir::new().unwrap();
2757 let root = dir.path().join("repo");
2758 std::fs::create_dir_all(&root).unwrap();
2759 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2760 let repo = store.repo_id.clone();
2761 for (path, prefix) in [("a/mod.py", "zeta"), ("b/mod.py", "alpha")] {
2762 for i in 0..20 {
2763 let name = format!("{prefix}_{i:02}");
2764 let id = symbol_id(&repo, path, &name);
2765 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name);
2766 e.attr("kind", serde_json::json!("function"));
2767 e.attr("file", serde_json::json!(path));
2768 e.attr("signature", serde_json::json!("def f(x): ..."));
2769 e.attr("exported", serde_json::json!(false));
2770 e.attr("start_line", serde_json::json!(1u32));
2771 e.attr("end_line", serde_json::json!(10u32));
2772 store.insert_entity(&e, &[path.to_string()]).unwrap();
2773 }
2774 }
2775 (dir, store)
2776 }
2777
2778 fn huge_signature_fixture() -> (tempfile::TempDir, Store) {
2783 let dir = tempfile::TempDir::new().unwrap();
2784 let root = dir.path().join("repo");
2785 std::fs::create_dir_all(&root).unwrap();
2786 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2787 let repo = store.repo_id.clone();
2788 let path = "api/app.py";
2789 let id = symbol_id(&repo, path, "big_fn");
2790 let mut e = Entity::new(id.clone(), kinds::SYMBOL, "big_fn".to_string());
2791 e.attr("kind", serde_json::json!("function"));
2792 e.attr("file", serde_json::json!(path));
2793 let mut decl = String::from("def big_fn(\n");
2794 for i in 0..400 {
2795 decl.push_str(&format!(" arg_{i:03}: str,\n"));
2796 }
2797 decl.push_str(") -> None:\n");
2798 e.attr("decl_header", serde_json::json!(decl));
2799 e.attr("exported", serde_json::json!(true));
2800 e.attr("start_line", serde_json::json!(1u32));
2801 e.attr("end_line", serde_json::json!(10u32));
2802 e.attr("entrypoints", serde_json::json!(["http: POST /big"]));
2807 store.insert_entity(&e, &[path.to_string()]).unwrap();
2808 (dir, store)
2809 }
2810
2811 #[test]
2812fn compiles_surface_map_with_attribution() {
2814 let (_dir, store) = fixture_store();
2815 let ctx = make_ctx(&store);
2816 let map = compile_surface_map(&ctx);
2817
2818 assert_eq!(map.entries.len(), 7);
2819 assert_eq!(map.repository, "repo");
2820 assert_eq!(map.revision, "not-indexed");
2821 assert!(map.token_count > 0);
2822
2823 let cls = entry(&map, "UserService");
2824 assert_eq!(cls.kind, SurfaceKind::Class);
2825 assert_eq!(cls.visibility, Visibility::Public);
2826 assert!(cls.exported);
2827 assert_eq!(cls.source_signature, "class UserService");
2828 assert_eq!(cls.range.path, "api/app.py");
2829 assert_eq!(cls.component.as_deref(), Some("api"));
2830 assert_eq!(cls.subsystem.as_deref(), Some("core"));
2831
2832 let ctor = entry(&map, "UserService.UserService");
2833 assert_eq!(ctor.kind, SurfaceKind::Constructor);
2834 assert_eq!(ctor.visibility, Visibility::Public);
2835 assert_eq!(ctor.semantic_signature.parameters[0].name, "name");
2836 assert_eq!(ctor.semantic_signature.parameters[0].ty.as_deref(), Some("String"));
2837
2838 let get = entry(&map, "UserService.get");
2839 assert_eq!(get.kind, SurfaceKind::Method);
2840 assert_eq!(get.visibility, Visibility::Public);
2841 assert_eq!(get.annotations, vec!["RestController"]);
2842 assert_eq!(get.flows, vec!["signup"]);
2843 assert_eq!(get.callers, vec!["create_user"]);
2844 assert!(get.callees.is_empty());
2845 let gs = &get.semantic_signature;
2846 assert_eq!(gs.parameters[0].name, "id");
2847 assert_eq!(gs.parameters[0].ty.as_deref(), Some("String"));
2848 assert!(!gs.parameters[0].receiver);
2849 assert_eq!(gs.returns.as_deref(), Some("User"));
2850 assert!(gs.constraints.iter().any(|c| c == "NotFound"));
2851 assert_eq!(get.provenance, Provenance::Extracted);
2852 assert_eq!(get.confidence, 0.85);
2853
2854 let hash = entry(&map, "UserService.hash");
2855 assert_eq!(hash.visibility, Visibility::Package);
2856
2857 let update = entry(&map, "UserService.update");
2858 assert_eq!(update.visibility, Visibility::Private);
2859 assert_eq!(update.modifiers, vec!["async"]);
2860 assert_eq!(update.state_authorities, vec!["cursor"]);
2861 let us = &update.semantic_signature;
2862 assert!(us.async_);
2863 assert!(us.parameters[0].receiver);
2864 assert_eq!(us.parameters[1].name, "patch");
2865 assert_eq!(us.returns.as_deref(), Some("bool"));
2866
2867 let create = entry(&map, "create_user");
2868 assert_eq!(create.kind, SurfaceKind::Function);
2869 assert_eq!(create.visibility, Visibility::Public);
2870 assert!(create.exported);
2871 assert_eq!(create.canonical_signature, "def create_user(name: str, age: int = 0) -> user");
2872 assert_eq!(create.contracts, vec!["http: GET /api/users"]);
2873 assert_eq!(create.callees, vec!["UserService.get"]);
2874 assert_eq!(create.invocation_surfaces, vec![
2875 "http: GET /api/users",
2876 "public_api: export:create_user (function)",
2877 ]);
2878 assert_eq!(create.semantic_signature.parameters[1].name, "age");
2879 assert_eq!(create.semantic_signature.parameters[1].default.as_deref(), Some("0"));
2880 assert_eq!(create.semantic_signature.returns.as_deref(), Some("User"));
2881
2882 let db = entry(&map, "db");
2883 assert_eq!(db.visibility, Visibility::Private);
2884 assert_eq!(db.state_authorities, vec!["sessions"]);
2885 assert_eq!(db.provenance, Provenance::Resolved);
2886 assert_eq!(db.confidence, 1.0);
2887 assert_eq!(db.semantic_signature.returns.as_deref(), Some("DB"));
2888 assert_eq!(db.flows, vec!["signup"]);
2889 }
2890
2891 #[test]
2892fn renderer_groups_and_budget_cuts() {
2894 let (_dir, store) = fixture_store();
2895 let ctx = make_ctx(&store);
2896 let map = compile_surface_map(&ctx);
2897
2898 let full = render_surface_map(&map, None);
2899 assert!(full.starts_with("SCC SYSTEM SURFACE MAP\n\n"));
2900 assert!(full.contains("API\n\napi/app.py [core]"));
2901 assert!(full.contains(" class UserService\n"));
2902 assert!(full.contains(" function create_user\n"));
2903 assert!(full.contains(" constructor UserService\n"));
2904 assert!(full.contains("Used by:"));
2905 assert!(full.contains("http: GET /api/users"));
2906 assert!(!full.contains("OMITTED"));
2907 assert_eq!(map.token_count, estimate_tokens(&full));
2908
2909 let tiny = render_surface_map(&map, Some(1));
2910 assert!(tiny.contains("OMITTED (token budget exceeded):"));
2911 assert!(tiny.contains("3 lower-ranked method definitions"));
2912 assert!(tiny.contains("1 lower-ranked class definitions"));
2913 assert!(tiny.contains("1 lower-ranked constructor definitions"));
2914 assert!(tiny.contains("2 lower-ranked function definitions"));
2915 assert!(!tiny.contains(" class UserService\n"));
2916 }
2917
2918 #[test]
2919fn semantic_parser_never_panics() {
2921 for sig in &["", " ", "(", ")", "()", "=>", "->", "(((", "fn", "public", "= 42"] {
2922 let s = parse_signature(sig, "fallback", None);
2923 assert_eq!(s.name, "fallback");
2924 }
2925 }
2926
2927 #[test]
2928fn semantic_parser_language_tolerant() {
2930 let s = parse_signature(
2932 "pub async fn render<T: Bound>(&self, x: T) -> String where T: Clone + Send",
2933 "render",
2934 Some("Widget"),
2935 );
2936 assert_eq!(s.name, "render");
2937 assert!(s.async_);
2938 assert!(s.generic_parameters.contains(&"T".to_string()));
2939 assert!(s.constraints.contains(&"T: Bound".to_string()));
2940 assert!(s.constraints.contains(&"T: Clone + Send".to_string()));
2941 assert_eq!(s.visibility, Some(Visibility::Public));
2942 assert_eq!(s.returns.as_deref(), Some("String"));
2943 assert!(s.parameters[0].receiver);
2944 assert_eq!(s.parameters[1].ty.as_deref(), Some("T"));
2945 assert_eq!(s.owner.as_deref(), Some("Widget"));
2946
2947 let g = parse_signature(
2949 "func (s *Store) Get(ctx context.Context) (User, error)",
2950 "Get",
2951 Some("Store"),
2952 );
2953 assert_eq!(g.name, "Get");
2954 assert_eq!(g.owner.as_deref(), Some("Store"));
2955 assert_eq!(g.parameters[0].ty.as_deref(), Some("context.Context"));
2956 assert_eq!(g.returns.as_deref(), Some("User, error"));
2957 }
2958
2959 #[test]
2962fn global_render_selects_within_budget_and_reports_omissions() {
2964 let (_dir, store) = fixture_store();
2965 let ctx = make_ctx(&store);
2966 let map = compile_surface_map(&ctx);
2967 let n = map.entries.len();
2968 assert!(n >= 5);
2969
2970 let big = select_and_render_global(&ctx, 100_000);
2972 assert_eq!(big.rendered_ids.len(), n);
2973 assert!(big.omitted_ids.is_empty());
2974 assert!(big.omissions.is_empty());
2975 assert!(big.text.starts_with("SCC SYSTEM SURFACE MAP"));
2976 assert!(big.token_count > 0);
2977
2978 let tiny = select_and_render_global(&ctx, 1);
2981 assert!(
2982 !tiny.rendered_ids.is_empty(),
2983 "required (invocation-surface) entries must survive a tiny budget"
2984 );
2985 for id in &tiny.rendered_ids {
2986 assert!(map.entries.iter().any(|e| &e.id == id), "{id} must be a candidate");
2987 }
2988 assert_eq!(tiny.rendered_ids.len() + tiny.omitted_ids.len(), n);
2990 for id in &tiny.omitted_ids {
2991 assert!(!tiny.rendered_ids.contains(id), "{id} both rendered and omitted");
2992 }
2993 let omitted_total: usize = tiny.omissions.iter().map(|o| o.count).sum();
2994 assert_eq!(omitted_total, tiny.omitted_ids.len());
2995
2996 let tiny2 = select_and_render_global(&ctx, 1);
2998 assert_eq!(tiny2.text, tiny.text);
2999 assert_eq!(tiny2.rendered_ids, tiny.rendered_ids);
3000 assert_eq!(tiny2.omitted_ids, tiny.omitted_ids);
3001 }
3002
3003 #[test]
3004fn task_render_skips_visible_unchanged_entries() {
3006 let (_dir, store) = fixture_store();
3007 let ctx = make_ctx(&store);
3008 let map = compile_surface_map(&ctx);
3009 let n = map.entries.len();
3010
3011 let create = entry(&map, "create_user");
3012 let mut visible = ContextLedger::default();
3013 visible.visible_symbols.insert(create.symbol_id.clone());
3014
3015 let out = select_and_render_task(&ctx, "create user", 100_000, &visible);
3016 assert!(
3018 !out.rendered_ids.contains(&create.id),
3019 "visible unchanged entries must not be re-injected"
3020 );
3021 assert_eq!(out.rendered_ids.len(), n - 1);
3023 assert_eq!(out.omitted_ids.len(), 1);
3024 assert!(out.omitted_ids.contains(&create.id));
3025 assert!(out.text.starts_with("SCC SYSTEM SURFACE MAP"));
3026 }
3027
3028 #[test]
3029fn task_render_never_omits_required_even_at_zero_budget() {
3031 let (_dir, store) = fixture_store();
3032 let ctx = make_ctx(&store);
3033 let out = select_and_render_task(&ctx, "user", 0, &ContextLedger::default());
3034 assert!(!out.rendered_ids.is_empty());
3037 assert!(!out.omissions.is_empty() || out.omitted_ids.is_empty());
3038 let rendered: BTreeSet<String> = out.rendered_ids.iter().cloned().collect();
3039 for id in &out.omitted_ids {
3040 assert!(!rendered.contains(id));
3041 }
3042 }
3043
3044 #[test]
3047fn file_importance_boosts_manifest_symbols_without_injecting_text() {
3049 assert_eq!(file_importance("package.json"), 1.0);
3053 assert_eq!(file_importance("Dockerfile"), 1.0);
3054 assert_eq!(file_importance(".github/workflows/ci.yml"), 1.0);
3055 assert_eq!(file_importance("src/main.rs"), 1.0);
3056 assert_eq!(file_importance("src/util.rs"), 0.0);
3057 assert_eq!(file_importance("src/main.py"), 1.0);
3058 assert_eq!(file_importance("Makefile"), 1.0);
3059 assert_eq!(file_importance("services/transcripts.py"), 0.0);
3061 }
3062
3063 #[test]
3064fn build_surface_global_matches_historical_pipeline() {
3066 let (_dir, store) = fixture_store();
3067 let ctx = make_ctx(&store);
3068 let map = compile_surface_map(&ctx);
3069 let n = map.entries.len();
3070 assert!(n >= 5);
3071
3072 let req = |budget: usize| SurfaceRequest {
3073 mode: SurfaceMode::Global,
3074 budget,
3075 explain: false,
3076 policy: SurfacePolicy::defaults(budget),
3077 semantic: None,
3078 };
3079
3080 let big = build_surface(&ctx, req(100_000));
3084 let legacy = select_and_render_global(&ctx, 100_000);
3085 assert_eq!(big.text, legacy.text);
3086 assert_eq!(big.rendered_ids, legacy.rendered_ids);
3087 assert_eq!(big.omitted_ids, legacy.omitted_ids);
3088 let mut big_ids = big.rendered_ids.clone();
3094 big_ids.sort();
3095 let mut map_ids: Vec<String> = map.entries.iter().map(|e| e.id.clone()).collect();
3096 map_ids.sort();
3097 assert_eq!(big_ids, map_ids, "the selected subset must cover every candidate");
3098 assert_eq!(big.rendered_ids.len(), n);
3099 assert!(big.omitted_ids.is_empty());
3100
3101 let tiny = build_surface(&ctx, req(1));
3104 assert!(!tiny.rendered_ids.is_empty());
3105 assert_eq!(tiny.rendered_ids.len() + tiny.omitted_ids.len(), n);
3106 let tiny2 = build_surface(&ctx, req(1));
3107 assert_eq!(tiny.text, tiny2.text);
3108 assert_eq!(tiny.rendered_ids, tiny2.rendered_ids);
3109 }
3110
3111 #[test]
3112fn build_surface_task_applies_novelty_suppression() {
3114 let (_dir, store) = fixture_store();
3115 let ctx = make_ctx(&store);
3116 let map = compile_surface_map(&ctx);
3117 let create = entry(&map, "create_user");
3118
3119 let mut visible = ContextLedger::default();
3120 visible.visible_symbols.insert(create.symbol_id.clone());
3121 let out = build_surface(
3122 &ctx,
3123 SurfaceRequest {
3124 mode: SurfaceMode::Task {
3125 goal: "create user",
3126 visible: Some(&visible),
3127 },
3128 budget: 100_000,
3129 explain: false,
3130 policy: SurfacePolicy::defaults(100_000),
3131 semantic: None,
3132 },
3133 );
3134 assert!(!out.rendered_ids.contains(&create.id));
3137 assert_eq!(out.rendered_ids.len(), map.entries.len() - 1);
3138 assert!(out.omitted_ids.contains(&create.id));
3139 let legacy = select_and_render_task(&ctx, "create user", 100_000, &visible);
3141 assert_eq!(out.text, legacy.text);
3142 assert_eq!(out.rendered_ids, legacy.rendered_ids);
3143
3144 let full = build_surface(
3146 &ctx,
3147 SurfaceRequest {
3148 mode: SurfaceMode::Task {
3149 goal: "create user",
3150 visible: None,
3151 },
3152 budget: 100_000,
3153 explain: false,
3154 policy: SurfacePolicy::defaults(100_000),
3155 semantic: None,
3156 },
3157 );
3158 assert_eq!(full.rendered_ids.len(), map.entries.len());
3159 }
3160
3161 #[test]
3162fn token_aware_quotas_cap_the_dominant_kind() {
3164 let dir = tempfile::TempDir::new().unwrap();
3169 let root = dir.path().join("repo");
3170 std::fs::create_dir_all(&root).unwrap();
3171 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3172 let repo = store.repo_id.clone();
3173 let path = "api/app.py";
3174 let mut rel_id: u64 = 10_000;
3175 for i in 0..900 {
3176 let name = format!("core_fn_{i:04}");
3177 let id = symbol_id(&repo, path, &name);
3178 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name);
3179 e.attr("kind", serde_json::json!("function"));
3180 e.attr("file", serde_json::json!(path));
3181 e.attr("signature", serde_json::json!("def f(x): ..."));
3182 e.attr("exported", serde_json::json!(false));
3183 e.attr("start_line", serde_json::json!(1u32));
3184 e.attr("end_line", serde_json::json!(10u32));
3185 store.insert_entity(&e, &[path.to_string()]).unwrap();
3186 }
3187 for i in 0..100 {
3188 let name = format!("state_fn_{i:04}");
3189 let id = symbol_id(&repo, path, &name);
3190 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name);
3191 e.attr("kind", serde_json::json!("function"));
3192 e.attr("file", serde_json::json!(path));
3193 e.attr("signature", serde_json::json!("def f(x): ..."));
3194 e.attr("exported", serde_json::json!(false));
3195 e.attr("start_line", serde_json::json!(1u32));
3196 e.attr("end_line", serde_json::json!(10u32));
3197 store.insert_entity(&e, &[path.to_string()]).unwrap();
3198 let state_id = entity_id(&repo, kinds::STATE, &format!("st{i:04}"));
3199 store
3200 .insert_entity(
3201 &Entity::new(state_id.clone(), kinds::STATE, format!("st{i:04}")),
3202 &[path.to_string()],
3203 )
3204 .unwrap();
3205 rel_id += 1;
3206 store
3207 .insert_relationship(
3208 &Relationship::new(
3209 relationship_id(rel_id),
3210 id,
3211 predicates::OWNS,
3212 state_id,
3213 Provenance::Extracted,
3214 ),
3215 path,
3216 )
3217 .unwrap();
3218 }
3219 let ctx = make_ctx(&store);
3220 let map = compile_surface_map(&ctx);
3221 assert_eq!(map.entries.len(), 1000);
3222
3223 let budget = 2000usize;
3224 let hard_max = SurfacePolicy::defaults(budget).hard_max;
3225 let req = |quotas: bool| SurfaceRequest {
3226 mode: SurfaceMode::Global,
3227 budget,
3228 explain: false,
3229 policy: SurfacePolicy {
3230 quotas,
3231 mmr: false,
3232 coverage: true,
3233 hard_max,
3234 },
3235 semantic: None,
3236 };
3237 let naive = build_surface(&ctx, req(false));
3238 let balanced = build_surface(&ctx, req(true));
3239 let count = |r: &scc_core::SurfaceRenderResult, prefix: &str| {
3240 r.rendered_ids.iter().filter(|id| id.contains(prefix)).count()
3241 };
3242 let naive_core = count(&naive, "core_fn_");
3243 let bal_core = count(&balanced, "core_fn_");
3244 let bal_state = count(&balanced, "state_fn_");
3245 assert!(
3246 bal_core < naive_core,
3247 "quotas must cut the dominant kind: {bal_core} !< {naive_core}"
3248 );
3249 assert!(bal_state > 0, "quotas must admit the under-represented kind");
3250 }
3251
3252 #[test]
3253fn hard_max_soft_overflow_renders_but_required_overflow_compresses() {
3255 let (_dir, store) = fixture_store();
3259 let ctx = make_ctx(&store);
3260 let soft = build_surface(
3261 &ctx,
3262 SurfaceRequest {
3263 mode: SurfaceMode::Global,
3264 budget: 5,
3265 explain: false,
3266 policy: SurfacePolicy::defaults(5), semantic: None,
3268 },
3269 );
3270 assert!(!soft.rendered_ids.is_empty());
3271 assert!(
3272 soft.text.contains("Used by:"),
3273 "under the hard max the full entry blocks must render"
3274 );
3275 assert!(soft.token_count > 5, "required may exceed the soft budget");
3276 assert!(soft.token_count <= 505, "required never exceeds the hard max");
3277
3278 let (_dir2, store2) = huge_signature_fixture();
3282 let ctx2 = make_ctx(&store2);
3283 let hard = build_surface(
3284 &ctx2,
3285 SurfaceRequest {
3286 mode: SurfaceMode::Global,
3287 budget: 100,
3288 explain: false,
3289 policy: SurfacePolicy::defaults(100), semantic: None,
3291 },
3292 );
3293 assert_eq!(hard.rendered_ids.len(), 1, "the required entry is never dropped");
3294 assert!(
3295 hard.text.contains("function big_fn"),
3296 "the compressed entry keeps its identity"
3297 );
3298 assert!(!hard.text.contains("Used by:"), "metadata sections are dropped");
3299 assert!(hard.token_count <= 600, "the compressed render fits the hard max");
3300 }
3301
3302 #[test]
3303fn staged_toggles_change_output_deterministically() {
3305 let (_dir, store) = zeta_alpha_fixture();
3306 let ctx = make_ctx(&store);
3307
3308 let budget = 80usize;
3310 let hard_max = SurfacePolicy::defaults(budget).hard_max;
3311 let req = |_stages: &SurfacePipelineStages| SurfaceRequest {
3312 mode: SurfaceMode::Task {
3313 goal: "zeta",
3314 visible: None,
3315 },
3316 budget,
3317 explain: false,
3318 policy: SurfacePolicy {
3319 quotas: false,
3320 mmr: true,
3321 coverage: true,
3322 hard_max,
3323 },
3324 semantic: None,
3325 };
3326 let render =
3327 |stages: &SurfacePipelineStages| build_surface_staged(&ctx, req(stages), stages);
3328 let has_b = |r: &scc_core::SurfaceRenderResult| {
3329 r.rendered_ids.iter().any(|id| id.contains("b/mod.py"))
3330 };
3331
3332 let full = render(&SurfacePipelineStages::default());
3333 let full2 = render(&SurfacePipelineStages::default());
3335 assert_eq!(full.text, full2.text);
3336 assert_eq!(full.rendered_ids, full2.rendered_ids);
3337
3338 let no_mmr_stages = SurfacePipelineStages {
3341 mmr: false,
3342 ..SurfacePipelineStages::default()
3343 };
3344 let no_mmr = render(&no_mmr_stages);
3345 assert_ne!(full.text, no_mmr.text);
3346 assert!(has_b(&full), "MMR must diversify across paths");
3347 assert!(!has_b(&no_mmr), "rank-order cut must stay on the first path");
3348
3349 let no_lex_stages = SurfacePipelineStages {
3352 lexical: false,
3353 ..SurfacePipelineStages::default()
3354 };
3355 let no_lex = build_surface_staged(
3356 &ctx,
3357 SurfaceRequest {
3358 mode: SurfaceMode::Task {
3359 goal: "zeta",
3360 visible: None,
3361 },
3362 budget: 100_000,
3363 explain: false,
3364 policy: SurfacePolicy::defaults(100_000),
3365 semantic: None,
3366 },
3367 &no_lex_stages,
3368 );
3369 let lex_on = build_surface_staged(
3370 &ctx,
3371 SurfaceRequest {
3372 mode: SurfaceMode::Task {
3373 goal: "zeta",
3374 visible: None,
3375 },
3376 budget: 100_000,
3377 explain: false,
3378 policy: SurfacePolicy::defaults(100_000),
3379 semantic: None,
3380 },
3381 &SurfacePipelineStages::default(),
3382 );
3383 assert_eq!(
3384 no_lex.rendered_ids.len(),
3385 20,
3386 "pure lexical skips unmatched entries"
3387 );
3388 assert_eq!(lex_on.rendered_ids.len(), 40, "the PPR blend ranks the full pool");
3389 assert_ne!(no_lex.text, lex_on.text);
3390 }
3391
3392 #[test]
3395fn overload_entries_get_distinct_ids_and_logical_symbol_id() {
3397 let dir = tempfile::TempDir::new().unwrap();
3398 let root = dir.path().join("repo");
3399 std::fs::create_dir_all(&root).unwrap();
3400 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3401 let repo = store.repo_id.clone();
3402 let path = "api/app.py";
3403 let base = symbol_id(&repo, path, "handle");
3404 for (n, id) in [(0u64, base.clone()), (1, format!("{}#1", base))] {
3405 let mut e = Entity::new(id.clone(), kinds::SYMBOL, "handle");
3406 e.attr("kind", serde_json::json!("function"));
3407 e.attr("file", serde_json::json!(path));
3408 e.attr("signature", serde_json::json!("def handle(x): ..."));
3409 e.attr("exported", serde_json::json!(true));
3410 e.attr("start_line", serde_json::json!(1u32));
3411 e.attr("end_line", serde_json::json!(10u32));
3412 e.attr("overload_index", serde_json::json!(n));
3413 store.insert_entity(&e, &[path.to_string()]).unwrap();
3414 }
3415 let ctx = make_ctx(&store);
3416 let map = compile_surface_map(&ctx);
3417 let handles: Vec<&SurfaceEntry> = map
3418 .entries
3419 .iter()
3420 .filter(|e| e.qualified_name == "handle")
3421 .collect();
3422 assert_eq!(handles.len(), 2, "same-name overloads stay separate entries");
3423 let mut ids: Vec<String> = handles.iter().map(|e| e.id.clone()).collect();
3424 ids.sort();
3425 assert_eq!(
3426 ids,
3427 vec![format!("{}#overload0", base), format!("{}#overload1", base)]
3428 );
3429 for e in handles {
3430 assert_eq!(e.symbol_id, base, "symbol_id stays the logical symbol");
3431 }
3432 }
3433
3434 #[test]
3435fn decl_header_wins_over_legacy_signature() {
3437 let dir = tempfile::TempDir::new().unwrap();
3438 let root = dir.path().join("repo");
3439 std::fs::create_dir_all(&root).unwrap();
3440 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3441 let repo = store.repo_id.clone();
3442 let path = "api/app.py";
3443 let id = symbol_id(&repo, path, "process");
3444 let mut e = Entity::new(id.clone(), kinds::SYMBOL, "process");
3445 e.attr("kind", serde_json::json!("function"));
3446 e.attr("file", serde_json::json!(path));
3447 e.attr("signature", serde_json::json!("def process(x) -> str"));
3448 e.attr(
3449 "decl_header",
3450 serde_json::json!("async def process(\n x: int,\n) -> str:"),
3451 );
3452 e.attr("exported", serde_json::json!(true));
3453 e.attr("start_line", serde_json::json!(1u32));
3454 e.attr("end_line", serde_json::json!(10u32));
3455 store.insert_entity(&e, &[path.to_string()]).unwrap();
3456 let ctx = make_ctx(&store);
3457 let map = compile_surface_map(&ctx);
3458 let proc = entry(&map, "process");
3459 assert_eq!(
3460 proc.source_signature,
3461 "async def process(\n x: int,\n) -> str:",
3462 "decl_header (exact header) must win over the legacy signature attr"
3463 );
3464 assert_eq!(proc.modifiers, vec!["async"]);
3465 }
3466
3467 fn symmetric_two_fn_fixture() -> (tempfile::TempDir, Store) {
3475 let dir = tempfile::TempDir::new().unwrap();
3476 let root = dir.path().join("repo");
3477 std::fs::create_dir_all(&root).unwrap();
3478 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3479 let repo = store.repo_id.clone();
3480 let path = "api/app.py";
3481 for name in ["alpha_fn", "beta_fn"] {
3482 let id = symbol_id(&repo, path, name);
3483 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name.to_string());
3484 e.attr("kind", serde_json::json!("function"));
3485 e.attr("file", serde_json::json!(path));
3486 e.attr("signature", serde_json::json!("def f(x): ..."));
3487 e.attr("exported", serde_json::json!(false));
3488 e.attr("start_line", serde_json::json!(1u32));
3489 e.attr("end_line", serde_json::json!(10u32));
3490 store.insert_entity(&e, &[path.to_string()]).unwrap();
3491 }
3492 (dir, store)
3493 }
3494
3495 fn pathological_signature_fixture() -> (tempfile::TempDir, Store) {
3502 let dir = tempfile::TempDir::new().unwrap();
3503 let root = dir.path().join("repo");
3504 std::fs::create_dir_all(&root).unwrap();
3505 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3506 let repo = store.repo_id.clone();
3507 let path = "api/app.py";
3508 let id = symbol_id(&repo, path, "big_fn");
3509 let mut e = Entity::new(id.clone(), kinds::SYMBOL, "big_fn".to_string());
3510 e.attr("kind", serde_json::json!("function"));
3511 e.attr("file", serde_json::json!(path));
3512 let mut decl = String::from("def big_fn(");
3513 for i in 0..400 {
3514 decl.push_str(&format!("arg_{i:03}: str, "));
3515 }
3516 decl.push_str(") -> None:");
3517 e.attr("decl_header", serde_json::json!(decl));
3518 e.attr("exported", serde_json::json!(true));
3519 e.attr("start_line", serde_json::json!(1u32));
3520 e.attr("end_line", serde_json::json!(10u32));
3521 e.attr("entrypoints", serde_json::json!(["http: POST /big"]));
3522 store.insert_entity(&e, &[path.to_string()]).unwrap();
3523 (dir, store)
3524 }
3525
3526 struct ZeroScorer;
3530impl crate::rank::SemanticScorer for ZeroScorer {
3532fn score(&self, _goal: &str, _entity: &scc_core::Entity) -> f64 {
3534 0.0
3535 }
3536 }
3537
3538 struct NameScorer {
3541 target: &'static str,
3542 score: f64,
3543 }
3544impl crate::rank::SemanticScorer for NameScorer {
3546fn score(&self, _goal: &str, entity: &scc_core::Entity) -> f64 {
3548 if entity.name.contains(self.target) {
3549 self.score
3550 } else {
3551 0.0
3552 }
3553 }
3554 }
3555
3556 fn explain_blocks(text: &str) -> Vec<(String, BTreeMap<String, f64>)> {
3563 const KINDS: [&str; 11] = [
3564 "function", "method", "constructor", "class", "interface",
3565 "trait", "type", "enum", "const", "module", "record",
3566 ];
3567 let mut blocks: Vec<(String, BTreeMap<String, f64>)> = Vec::new();
3568 for line in text.lines() {
3569 if !line.starts_with(" ") {
3573 if let Some(rest) = line.strip_prefix(" ") {
3574 let first = rest.split_whitespace().next().unwrap_or("");
3575 if KINDS.contains(&first) {
3576 let name = rest.split_whitespace().nth(1).unwrap_or("").to_string();
3577 blocks.push((name, BTreeMap::new()));
3578 continue;
3579 }
3580 }
3581 }
3582 if let Some((_, comps)) = blocks.last_mut() {
3583 if let Some((k, v)) = line.trim_start().split_once(':') {
3584 if let Ok(num) = v.trim().parse::<f64>() {
3585 comps.insert(k.trim().to_string(), num);
3586 }
3587 }
3588 }
3589 }
3590 blocks
3591 }
3592
3593 #[test]
3594fn explain_renders_full_score_decomposition() {
3596 let (_dir, store) = fixture_store();
3597 let ctx = make_ctx(&store);
3598 let request = SurfaceRequest {
3599 mode: SurfaceMode::Task {
3600 goal: "create user",
3601 visible: None,
3602 },
3603 budget: 100_000,
3604 explain: true,
3605 policy: SurfacePolicy::defaults(100_000),
3606 semantic: None,
3607 };
3608 let out = build_surface(&ctx, request);
3609 for key in [
3612 "importance:",
3613 "task_ppr:",
3614 "global_ppr:",
3615 "lexical:",
3616 "semantic:",
3617 "confidence:",
3618 "criticality:",
3619 "change_risk:",
3620 "novelty:",
3621 "because:",
3622 ] {
3623 assert!(out.text.contains(key), "explain must render {key:?}");
3624 }
3625 assert!(out.text.contains("task seed:"), "seed evidence -> reason");
3627 assert!(out.text.contains("primary flow participant"), "flow evidence -> reason");
3628 assert!(out.text.contains("public component surface"), "visibility evidence -> reason");
3629 assert!(out.text.contains("owns "), "state-authority evidence -> reason");
3630 assert!(out.text.contains("concrete invocation surface"), "invocation evidence -> reason");
3631 for (_, comps) in explain_blocks(&out.text) {
3633 assert_eq!(comps.get("semantic").copied().unwrap_or(-1.0), 0.0);
3634 }
3635 let out2 = build_surface(&ctx, request);
3637 assert_eq!(out.text, out2.text);
3638 }
3639
3640 #[test]
3641fn semantic_none_redistributes_and_some_reranks() {
3643 let (_dir, store) = symmetric_two_fn_fixture();
3644 let ctx = make_ctx(&store);
3645 let alpha = symbol_id(&store.repo_id, "api/app.py", "alpha_fn");
3646 let beta = symbol_id(&store.repo_id, "api/app.py", "beta_fn");
3647
3648 let none = build_surface(
3650 &ctx,
3651 SurfaceRequest {
3652 mode: SurfaceMode::Global,
3653 budget: 100_000,
3654 explain: true,
3655 policy: SurfacePolicy::defaults(100_000),
3656 semantic: None,
3657 },
3658 );
3659 let zero = build_surface(
3660 &ctx,
3661 SurfaceRequest {
3662 mode: SurfaceMode::Global,
3663 budget: 100_000,
3664 explain: true,
3665 policy: SurfacePolicy::defaults(100_000),
3666 semantic: Some(&ZeroScorer),
3667 },
3668 );
3669 let none_blocks = explain_blocks(&none.text);
3670 let zero_blocks = explain_blocks(&zero.text);
3671 assert!(!none_blocks.is_empty());
3672 assert_eq!(none_blocks.len(), zero_blocks.len());
3673 for ((nname, ncomps), (zname, zcomps)) in none_blocks.iter().zip(&zero_blocks) {
3677 assert_eq!(nname, zname);
3678 let n = ncomps.get("importance").copied().unwrap();
3679 let z = zcomps.get("importance").copied().unwrap();
3680 assert!(
3681 n > z,
3682 "renormalized total must exceed the phantom-hole total for {nname}"
3683 );
3684 let blend = crate::pagerank::final_importance(
3688 ncomps.get("task_ppr").copied().unwrap_or(0.0),
3689 ncomps.get("global_ppr").copied().unwrap_or(0.0),
3690 ncomps.get("lexical").copied().unwrap_or(0.0),
3691 0.0,
3692 ncomps.get("confidence").copied().unwrap_or(0.0),
3693 ncomps.get("criticality").copied().unwrap_or(0.0),
3694 ncomps.get("change_risk").copied().unwrap_or(0.0),
3695 0.0,
3696 false, );
3698 let expected = blend * REDISTRIBUTION_SCALE
3699 + crate::pagerank::NOVELTY_WEIGHT
3700 * ncomps.get("novelty").copied().unwrap_or(0.0);
3701 assert!(
3702 (n - expected).abs() < 0.002,
3703 "{nname}: rendered {n:.6} != renormalized {expected:.6} (blend {blend:.6})"
3704 );
3705 }
3706
3707 let real = build_surface(
3711 &ctx,
3712 SurfaceRequest {
3713 mode: SurfaceMode::Global,
3714 budget: 100_000,
3715 explain: true,
3716 policy: SurfacePolicy::defaults(100_000),
3717 semantic: Some(&NameScorer {
3718 target: "beta_fn",
3719 score: 1.0,
3720 }),
3721 },
3722 );
3723 let blocks = explain_blocks(&real.text);
3724 assert_eq!(blocks.len(), 2);
3725 assert_eq!(blocks[0].0, "beta_fn", "semantic 10% must rerank beta first");
3726 let a = blocks
3727 .iter()
3728 .find(|(n, _)| *n == "alpha_fn")
3729 .and_then(|(_, c)| c.get("importance").copied())
3730 .unwrap();
3731 let b = blocks
3732 .iter()
3733 .find(|(n, _)| *n == "beta_fn")
3734 .and_then(|(_, c)| c.get("importance").copied())
3735 .unwrap();
3736 assert!(
3737 (b - a - crate::pagerank::SEMANTIC_WEIGHT).abs() < 0.002,
3738 "beta must lead alpha by the real 10%: {b:.6} - {a:.6}"
3739 );
3740 assert_eq!(none_blocks[0].0, "alpha_fn");
3742 assert_eq!(none.rendered_ids[0], alpha);
3743 assert_eq!(real.rendered_ids[0], beta);
3744 }
3745
3746 #[test]
3747fn pathological_required_entry_compresses_to_symbol_identity() {
3749 let (_dir, store) = pathological_signature_fixture();
3754 let ctx = make_ctx(&store);
3755 let hard_max = 20usize;
3756 let out = build_surface(
3757 &ctx,
3758 SurfaceRequest {
3759 mode: SurfaceMode::Global,
3760 budget: 1,
3761 explain: false,
3762 policy: SurfacePolicy {
3763 quotas: true,
3764 mmr: true,
3765 coverage: true,
3766 hard_max,
3767 },
3768 semantic: None,
3769 },
3770 );
3771 assert_eq!(out.rendered_ids.len(), 1, "the required entry is never dropped");
3772 assert!(out.text.contains("function big_fn"), "identity line must render");
3773 assert!(
3774 !out.text.contains("def big_fn("),
3775 "the 400-arg signature must be dropped at the identity level"
3776 );
3777 assert!(
3778 out.token_count <= hard_max,
3779 "hard-max invariant on the rendered text: {} > {hard_max}",
3780 out.token_count
3781 );
3782 }
3783}