1use scc_core::{
44 kinds, predicates, Provenance, ReferenceEdge, ReferenceKind, Relationship, SourceRange,
45 SurfaceEntry, TaskSeed,
46};
47use scc_graph::TrustedGraphView;
48use std::collections::{HashMap, HashSet};
49
50pub type EdgeAdjust = dyn for<'a, 'b, 'c> Fn(&'a str, &'b str, &'c str, f64) -> Option<(String, f64)>;
55
56fn no_adjust(_: &str, _: &str, _: &str, _: f64) -> Option<(String, f64)> {
58 None
59}
60
61pub const FLOW_PARTICIPATION: f64 = 1.5;
68pub const INVOCATION: f64 = 1.5;
70pub const PUBLIC_API: f64 = 1.4;
72pub const OWNS_STATE: f64 = 1.4;
74pub const PRODUCES_CONSUMES: f64 = 1.3;
76pub const IMPLEMENTS: f64 = 1.25;
78pub const EXTENDS: f64 = 1.2;
80pub const CALLS_RESOLVED: f64 = 1.2;
82pub const CONTRACT_PARTICIPATION: f64 = 1.2;
84pub const SUBSCRIBES_PUBLISHES: f64 = 1.2;
86pub const DEPENDS_ON: f64 = 1.0;
88pub const CALLS_EXTRACTED: f64 = 0.9;
90pub const IMPORTS: f64 = 0.6;
92pub const RANK_MEMBERSHIP: f64 = 0.4;
106pub const RANK_MEMBER_OF: f64 = 0.8;
107pub const RANK_PARTICIPATES: f64 = 0.6;
108pub const RANK_FLOW_REACHES_PARTICIPANT: f64 = 1.2;
109pub const RANK_HANDLES: f64 = 1.5;
110pub const RANK_DEFINES: f64 = 1.2;
111pub const RANK_HANDLED_BY: f64 = 1.5;
112pub const RANK_DEFINED_BY: f64 = 1.2;
113
114pub const PROV_OBSERVED: f64 = 1.0;
117pub const PROV_RESOLVED: f64 = 1.0;
118pub const PROV_EXTRACTED: f64 = 0.85;
119pub const PROV_DECLARED: f64 = 0.8;
120pub const PROV_INFERRED: f64 = 0.6;
121pub const PROV_STALE: f64 = 0.0;
122
123pub const DAMPING_FACTOR: f64 = 0.85;
125pub const POWER_ITERATIONS: usize = 50;
127pub const WARM_START_GLOBAL_BLEND: f64 = 0.3;
129
130pub const UBIQUITY_THRESHOLD: usize = 20;
133
134const GENERIC_NAMES: [&str; 7] = [
136 "utils", "logger", "errors", "common", "types", "helpers", "config",
137];
138
139const INVOCATION_KINDS: [&str; 5] = [
142 kinds::ROUTE,
143 kinds::ENDPOINT,
144 kinds::EVENT,
145 kinds::TOPIC,
146 kinds::QUEUE,
147];
148
149const CONTRACT_KINDS: [&str; 2] = [kinds::CONTRACT, kinds::REGISTRY];
151
152const RANKABLE_KINDS: [&str; 15] = [
159 kinds::SYMBOL,
160 kinds::COMPONENT,
161 kinds::SUBSYSTEM,
162 kinds::SERVICE,
163 kinds::SYSTEM,
164 kinds::FLOW,
165 kinds::CONTRACT,
166 kinds::STATE,
167 kinds::REACTIVE,
168 kinds::ROUTE,
169 kinds::TOPIC,
170 kinds::QUEUE,
171 kinds::DATA_STORE,
172 kinds::SCHEMA,
173 kinds::FILE,
174];
175
176const PROJECTION_PREDICATES: [&str; 8] = [
182 predicates::OWNS,
183 predicates::REGISTERS,
184 predicates::PUBLISHES,
185 predicates::READS,
186 predicates::WRITES,
187 predicates::PARTICIPATES_IN,
188 predicates::HANDLES,
189 predicates::DEFINES,
190];
191
192pub const PROJECTION_BONUS_FACTOR: f64 = 0.4;
196pub const PROJECTION_BONUS_CAP: f64 = 0.5;
199
200pub fn provenance_weight(provenance: Provenance) -> f64 {
207 match provenance {
208 Provenance::Observed => PROV_OBSERVED,
209 Provenance::Resolved => PROV_RESOLVED,
210 Provenance::Extracted => PROV_EXTRACTED,
211 Provenance::Declared => PROV_DECLARED,
212 Provenance::Inferred => PROV_INFERRED,
213 Provenance::Stale => PROV_STALE,
214 }
215}
216
217pub fn rarity(total_symbols: usize, in_degree: usize) -> f64 {
224 if total_symbols == 0 {
225 return 1.0;
226 }
227 let r = (total_symbols as f64 / (1 + in_degree) as f64).ln();
228 r.clamp(0.25, 1.5)
229}
230
231pub fn predicate_weight(predicate: &str, provenance: Provenance) -> f64 {
235 match predicate {
236 predicates::CALLS => match provenance {
237 Provenance::Resolved => CALLS_RESOLVED,
238 _ => CALLS_EXTRACTED,
239 },
240 predicates::INVOKES | predicates::HANDLES_CALLBACK => INVOCATION,
241 predicates::EXPORTS => PUBLIC_API,
242 predicates::OWNS => OWNS_STATE,
243 predicates::READS | predicates::WRITES => PRODUCES_CONSUMES,
244 predicates::IMPLEMENTS | predicates::IMPLEMENTED_BY => IMPLEMENTS,
245 predicates::INHERITS => EXTENDS,
246 predicates::REGISTERS => CONTRACT_PARTICIPATION,
247 predicates::PUBLISHES | predicates::SUBSCRIBES => SUBSCRIBES_PUBLISHES,
248 predicates::IMPORTS => IMPORTS,
249 predicates::CONTAINS => RANK_MEMBERSHIP,
250 _ => DEPENDS_ON,
251 }
252}
253
254pub fn edge_weight(
259 predicate: &str,
260 provenance: Provenance,
261 confidence: f64,
262 total_symbols: usize,
263 target_in_degree: usize,
264) -> f64 {
265 predicate_weight(predicate, provenance)
266 * provenance_weight(provenance)
267 * confidence.clamp(0.0, 1.0)
268 * rarity(total_symbols, target_in_degree)
269}
270
271fn reference_kind(predicate: &str, target_kind: &str) -> Option<ReferenceKind> {
281 match predicate {
282 predicates::CALLS | predicates::INVOKES | predicates::HANDLES_CALLBACK => {
283 Some(ReferenceKind::Call)
284 }
285 predicates::READS => Some(ReferenceKind::Read),
286 predicates::WRITES => Some(ReferenceKind::Write),
287 predicates::OWNS if target_kind == kinds::STATE || target_kind == kinds::REACTIVE => {
288 Some(ReferenceKind::Write)
289 }
290 predicates::IMPLEMENTS | predicates::IMPLEMENTED_BY => Some(ReferenceKind::Implement),
291 predicates::INHERITS => Some(ReferenceKind::Extend),
292 predicates::REGISTERS | predicates::PUBLISHES | predicates::SUBSCRIBES => {
293 Some(ReferenceKind::Register)
294 }
295 predicates::IMPORTS => Some(ReferenceKind::Import),
296 predicates::EXPORTS => Some(ReferenceKind::Export),
297 predicates::DECORATES | predicates::ANNOTATES => Some(ReferenceKind::Decorate),
298 _ => None,
299 }
300}
301
302fn locations_for(view: &TrustedGraphView, rel: &Relationship) -> Vec<SourceRange> {
309 let mut locs: Vec<SourceRange> = Vec::new();
310 for id in [&rel.subject, &rel.object] {
311 let Some(e) = view.entity(id) else { continue };
312 if e.kind != kinds::SYMBOL {
313 continue;
314 }
315 let Some(path) = e.attributes.get("file").and_then(|v| v.as_str()) else {
316 continue;
317 };
318 let start = e
319 .attributes
320 .get("start_line")
321 .and_then(|v| v.as_u64())
322 .unwrap_or(0) as u32;
323 let end = e
324 .attributes
325 .get("end_line")
326 .and_then(|v| v.as_u64())
327 .unwrap_or(0) as u32;
328 let start = start.max(1);
329 let loc = SourceRange::new(path, start, end.max(start));
330 if !locs.contains(&loc) {
331 locs.push(loc);
332 }
333 }
334 locs
335}
336
337pub fn build_reference_graph(view: &TrustedGraphView) -> Vec<ReferenceEdge> {
355 let mut out: Vec<ReferenceEdge> = Vec::new();
356 for rel in view.all_rels() {
357 let target_kind = view
358 .entity(&rel.object)
359 .map(|e| e.kind.as_str())
360 .unwrap_or("");
361 let kind = match reference_kind(&rel.predicate, target_kind) {
362 Some(k) => k,
363 None => continue,
364 };
365 let (source, target) = if rel.predicate == predicates::IMPLEMENTED_BY {
367 (rel.object.clone(), rel.subject.clone())
368 } else {
369 (rel.subject.clone(), rel.object.clone())
370 };
371 out.push(ReferenceEdge {
372 source_symbol: source,
373 target_symbol: target,
374 kind,
375 locations: locations_for(view, rel),
376 provenance: rel.provenance,
377 confidence: rel.confidence as f32,
378 });
379 }
380 out
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub enum RankEdgeKind {
400 Contains,
405 MemberOf,
409 ParticipatesIn,
411 FlowReachesParticipant,
415 Handles,
417 HandledBy,
421 Defines,
423 DefinedBy,
427}
428
429impl RankEdgeKind {
431 pub fn reverse_ranking_transition(self) -> Option<RankEdgeKind> {
439 match self {
440 RankEdgeKind::Contains => Some(RankEdgeKind::MemberOf),
441 RankEdgeKind::MemberOf => Some(RankEdgeKind::Contains),
442 RankEdgeKind::ParticipatesIn => Some(RankEdgeKind::FlowReachesParticipant),
443 RankEdgeKind::FlowReachesParticipant => Some(RankEdgeKind::ParticipatesIn),
444 RankEdgeKind::Handles => Some(RankEdgeKind::HandledBy),
445 RankEdgeKind::HandledBy => Some(RankEdgeKind::Handles),
446 RankEdgeKind::Defines => Some(RankEdgeKind::DefinedBy),
447 RankEdgeKind::DefinedBy => Some(RankEdgeKind::Defines),
448 }
449 }
450
451 pub fn weight(self) -> f64 {
454 match self {
455 RankEdgeKind::Contains => RANK_MEMBERSHIP,
456 RankEdgeKind::MemberOf => RANK_MEMBER_OF,
457 RankEdgeKind::ParticipatesIn => RANK_PARTICIPATES,
458 RankEdgeKind::FlowReachesParticipant => RANK_FLOW_REACHES_PARTICIPANT,
459 RankEdgeKind::Handles => RANK_HANDLES,
460 RankEdgeKind::HandledBy => RANK_HANDLED_BY,
461 RankEdgeKind::Defines => RANK_DEFINES,
462 RankEdgeKind::DefinedBy => RANK_DEFINED_BY,
463 }
464 }
465}
466
467const CONTAINMENT_HIERARCHY: [&str; 6] = [
477 kinds::SYSTEM,
478 kinds::SUBSYSTEM,
479 kinds::SERVICE,
480 kinds::COMPONENT,
481 kinds::FILE,
482 kinds::SYMBOL,
483];
484
485fn containment_position(kind: &str) -> Option<usize> {
489 CONTAINMENT_HIERARCHY.iter().position(|k| *k == kind)
490}
491
492pub fn rank_edge_kind(
505 predicate: &str,
506 subject_kind: &str,
507 target_kind: &str,
508) -> Option<RankEdgeKind> {
509 match predicate {
510 predicates::CONTAINS => {
511 let (Some(sp), Some(tp)) = (
512 containment_position(subject_kind),
513 containment_position(target_kind),
514 ) else {
515 return None;
516 };
517 if sp < tp {
518 Some(RankEdgeKind::Contains)
519 } else if tp < sp {
520 Some(RankEdgeKind::MemberOf)
521 } else {
522 None
523 }
524 }
525 predicates::PARTICIPATES_IN
526 if subject_kind == kinds::SYMBOL && target_kind == kinds::FLOW =>
527 {
528 Some(RankEdgeKind::ParticipatesIn)
529 }
530 predicates::PARTICIPATES_IN
531 if subject_kind == kinds::FLOW && target_kind == kinds::SYMBOL =>
532 {
533 Some(RankEdgeKind::FlowReachesParticipant)
534 }
535 predicates::HANDLES if subject_kind == kinds::SYMBOL && target_kind == kinds::ROUTE => {
536 Some(RankEdgeKind::Handles)
537 }
538 predicates::HANDLES if subject_kind == kinds::ROUTE && target_kind == kinds::SYMBOL => {
540 Some(RankEdgeKind::HandledBy)
541 }
542 predicates::DEFINES if subject_kind == kinds::SYMBOL && target_kind == kinds::SCHEMA => {
543 Some(RankEdgeKind::Defines)
544 }
545 predicates::DEFINES if subject_kind == kinds::SCHEMA && target_kind == kinds::SYMBOL => {
547 Some(RankEdgeKind::DefinedBy)
548 }
549 _ => None,
550 }
551}
552
553pub struct SystemRanker<'a> {
569 view: &'a TrustedGraphView<'a>,
570 nodes: Vec<String>,
572 kinds: Vec<String>,
574 index: HashMap<String, usize>,
575 adjacency: Vec<Vec<(usize, f64)>>,
577 in_degree: Vec<usize>,
579 projection: Vec<Vec<usize>>,
584 global: Vec<f64>,
586}
587
588impl<'a> SystemRanker<'a> {
590 pub fn new(view: &'a TrustedGraphView<'a>) -> SystemRanker<'a> {
593 Self::with_edge_adjust(view, no_adjust)
594 }
595
596 pub fn with_edge_adjust<F>(
603 view: &'a TrustedGraphView<'a>,
604 adjust: F,
605 ) -> SystemRanker<'a>
606 where
607 F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
608 {
609 let mut pairs: Vec<(String, String)> = view
610 .entities()
611 .filter(|e| RANKABLE_KINDS.contains(&e.kind.as_str()))
612 .map(|e| (e.id.clone(), e.kind.clone()))
613 .collect();
614 pairs.sort_by(|a, b| a.0.cmp(&b.0));
615 let nodes: Vec<String> = pairs.iter().map(|(id, _)| id.clone()).collect();
616 let kinds: Vec<String> = pairs.iter().map(|(_, k)| k.clone()).collect();
617 let index: HashMap<String, usize> = nodes
618 .iter()
619 .enumerate()
620 .map(|(i, s)| (s.clone(), i))
621 .collect();
622 let n = nodes.len();
623
624 let mut edges: Vec<(usize, usize, f64)> = Vec::new();
631 let mut in_sources: Vec<HashSet<usize>> = vec![HashSet::new(); n];
632 for rel in view.all_rels() {
633 let (Some(&si), Some(&ti)) = (index.get(&rel.subject), index.get(&rel.object)) else {
634 continue;
635 };
636 let subject_kind = view
637 .entity(&rel.subject)
638 .map(|e| e.kind.as_str())
639 .unwrap_or("");
640 let target_kind = view
641 .entity(&rel.object)
642 .map(|e| e.kind.as_str())
643 .unwrap_or("");
644 let reference_weight = if reference_kind(&rel.predicate, target_kind).is_some() {
647 Some(
648 predicate_weight(&rel.predicate, rel.provenance)
649 * provenance_weight(rel.provenance)
650 * rel.confidence.clamp(0.0, 1.0),
651 )
652 } else {
653 None
654 };
655 if let Some(kind) = rank_edge_kind(&rel.predicate, subject_kind, target_kind) {
659 let w = kind.weight()
660 * provenance_weight(rel.provenance)
661 * rel.confidence.clamp(0.0, 1.0);
662 if let Some(w) = Self::adjust_edge(&adjust, &rel.subject, &rel.predicate, &rel.object, w) {
663 in_sources[ti].insert(si);
664 edges.push((si, ti, w));
665 }
666 if let Some(rev) = kind.reverse_ranking_transition() {
667 let rw = rev.weight()
668 * provenance_weight(rel.provenance)
669 * rel.confidence.clamp(0.0, 1.0);
670 if let Some(rw) = Self::adjust_edge(&adjust, &rel.object, &rel.predicate, &rel.subject, rw) {
671 in_sources[si].insert(ti);
672 edges.push((ti, si, rw));
673 }
674 }
675 }
676 if let Some(w) = reference_weight {
677 if let Some(w) = Self::adjust_edge(&adjust, &rel.subject, &rel.predicate, &rel.object, w) {
678 in_sources[ti].insert(si);
679 edges.push((si, ti, w));
680 }
681 }
682 }
683 let in_degree: Vec<usize> = in_sources.iter().map(|s| s.len()).collect();
684
685 let mut agg: Vec<HashMap<usize, f64>> = vec![HashMap::new(); n];
687 for (si, ti, w) in edges {
688 let r = rarity(n, in_degree[ti]);
689 *agg[si].entry(ti).or_insert(0.0) += w * r;
690 }
691 let mut adjacency: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
692 for (i, row) in agg.iter().enumerate() {
693 let sum: f64 = row.values().sum();
694 if sum > 0.0 {
695 let mut v: Vec<(usize, f64)> =
696 row.iter().map(|(j, w)| (*j, w / sum)).collect();
697 v.sort_by_key(|(j, _)| *j);
698 adjacency[i] = v;
699 }
700 }
701
702 let mut projection: Vec<Vec<usize>> = vec![Vec::new(); n];
707 for rel in view.all_rels() {
708 if !PROJECTION_PREDICATES.contains(&rel.predicate.as_str()) {
709 continue;
710 }
711 let (Some(&si), Some(&ti)) = (index.get(&rel.subject), index.get(&rel.object)) else {
712 continue;
713 };
714 if kinds[ti] == kinds::SYMBOL {
715 continue; }
717 projection[si].push(ti);
718 }
719 for p in projection.iter_mut() {
720 p.sort_unstable();
721 p.dedup();
722 }
723
724 let ranker = SystemRanker {
725 view,
726 nodes,
727 kinds,
728 index,
729 adjacency,
730 in_degree,
731 projection,
732 global: Vec::new(),
733 };
734 let global = Self::ppr(&ranker.adjacency, &ranker.global_personalization());
735 let mut ranker = ranker;
736 ranker.global = global;
737 ranker
738 }
739
740 fn adjust_edge(
744 adjust: &impl for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
745 subject: &str,
746 predicate: &str,
747 object: &str,
748 base: f64,
749 ) -> Option<f64> {
750 match adjust(subject, predicate, object, base) {
751 None => Some(base),
752 Some((mode, value)) => match mode.as_str() {
753 "add" => Some(base + value),
754 "multiply" => Some(base * value),
755 "replace" => Some(value),
756 "veto" => None,
757 _ => Some(base),
758 }
759 .filter(|w| w.is_finite() && *w > 0.0),
760 }
761 }
762
763 pub fn nodes(&self) -> &[String] {
767 &self.nodes
768 }
769
770 pub fn symbols(&self) -> Vec<String> {
775 self.nodes
776 .iter()
777 .zip(self.kinds.iter())
778 .filter(|(_, k)| k.as_str() == kinds::SYMBOL)
779 .map(|(id, _)| id.clone())
780 .collect()
781 }
782
783 pub fn index_of(&self, id: &str) -> Option<usize> {
786 self.index.get(id).copied()
787 }
788
789 pub fn in_degree(&self, id: &str) -> usize {
792 self.index
793 .get(id)
794 .map(|i| self.in_degree[*i])
795 .unwrap_or(0)
796 }
797
798 pub fn global_vector(&self) -> Vec<f64> {
803 self.global.clone()
804 }
805
806 pub fn project_to_symbols(&self, vector: &[f64]) -> Vec<(String, f64)> {
818 let mut scores: Vec<f64> = Vec::with_capacity(self.nodes.len());
819 for (i, _id) in self.nodes.iter().enumerate() {
820 let own = vector.get(i).copied().unwrap_or(0.0);
821 let mut bonus = 0.0;
822 for &j in self.projection[i].iter() {
823 bonus += vector.get(j).copied().unwrap_or(0.0);
824 }
825 scores.push(own + (PROJECTION_BONUS_FACTOR * bonus).min(PROJECTION_BONUS_CAP));
826 }
827 let mut out: Vec<(String, f64)> = Vec::new();
828 for (i, id) in self.nodes.iter().enumerate() {
829 if self.kinds[i] == kinds::SYMBOL {
830 out.push((id.clone(), scores[i]));
831 }
832 }
833 out
834 }
835
836 pub fn task_vector(&self, seeds: &[TaskSeed]) -> Vec<f64> {
843 let n = self.nodes.len();
844 if n == 0 {
845 return Vec::new();
846 }
847 let mut s = vec![0.0; n];
848 let mut found = false;
849 for seed in seeds {
850 if let Some(&i) = self.index.get(&seed.id) {
851 s[i] += seed.weight.max(0.0);
852 found = true;
853 }
854 }
855 if !found {
856 return self.global.clone();
857 }
858 let sum: f64 = s.iter().sum();
859 if sum <= 0.0 {
860 return self.global.clone();
861 }
862 let s: Vec<f64> = s.iter().map(|v| v / sum).collect();
863 let mut start = vec![0.0; n];
864 for i in 0..n {
865 start[i] = WARM_START_GLOBAL_BLEND * self.global[i] + (1.0 - WARM_START_GLOBAL_BLEND) * s[i];
866 }
867 Self::ppr_with(&self.adjacency, &s, &start)
868 }
869
870 fn global_personalization(&self) -> Vec<f64> {
877 let n = self.nodes.len();
878 let mut seeds = vec![0.0; n];
879 let mut flow_eps: HashSet<String> = HashSet::new();
881 for f in self.view.flows() {
882 if let Some(ep) = f.attributes.get("entrypoint").and_then(|v| v.as_str()) {
883 flow_eps.insert(ep.to_string());
884 }
885 }
886 for (i, id) in self.nodes.iter().enumerate() {
887 let mut seed = false;
888 if let Some(e) = self.view.entity(id) {
889 let exported = e
890 .attributes
891 .get("exported")
892 .and_then(|v| v.as_bool())
893 .unwrap_or(false);
894 let has_entrypoints = e
895 .attributes
896 .get("entrypoints")
897 .and_then(|v| v.as_array())
898 .map(|a| !a.is_empty())
899 .unwrap_or(false);
900 seed = seed || exported || has_entrypoints;
901 }
902 if flow_eps.contains(id.as_str()) {
903 seed = true;
904 }
905 for r in self.view.out_edges(id) {
906 let tk = self
907 .view
908 .entity(&r.object)
909 .map(|e| e.kind.as_str())
910 .unwrap_or("");
911 match r.predicate.as_str() {
912 predicates::HANDLES if INVOCATION_KINDS.contains(&tk) => seed = true,
913 predicates::OWNS if tk == kinds::STATE || tk == kinds::REACTIVE => seed = true,
914 predicates::REGISTERS | predicates::PUBLISHES
915 if CONTRACT_KINDS.contains(&tk) =>
916 {
917 seed = true;
918 }
919 _ => {}
920 }
921 }
922 if seed {
923 seeds[i] = 1.0;
924 }
925 }
926 seeds
927 }
928
929 fn ppr_with(adjacency: &[Vec<(usize, f64)>], personalization: &[f64], start: &[f64]) -> Vec<f64> {
933 let n = adjacency.len();
934 if n == 0 {
935 return Vec::new();
936 }
937 let mut r = start.to_vec();
938 let d = DAMPING_FACTOR;
939 for _ in 0..POWER_ITERATIONS {
940 let mut nr = vec![0.0; n];
941 let mut dangling = 0.0;
942 for (i, row) in adjacency.iter().enumerate() {
943 let ri = r[i];
944 if row.is_empty() {
945 dangling += ri;
946 } else {
947 for (j, w) in row {
948 nr[*j] += ri * w;
949 }
950 }
951 }
952 let d_mass = d * dangling / n as f64;
953 for k in 0..n {
954 nr[k] = (1.0 - d) * personalization[k] + d * nr[k] + d_mass;
955 }
956 r = nr;
957 }
958 r
959 }
960
961 fn ppr(adjacency: &[Vec<(usize, f64)>], seeds: &[f64]) -> Vec<f64> {
965 let n = adjacency.len();
966 if n == 0 {
967 return Vec::new();
968 }
969 let sum: f64 = seeds.iter().sum();
970 let s: Vec<f64> = if sum > 0.0 {
971 seeds.iter().map(|v| v / sum).collect()
972 } else {
973 vec![1.0 / n as f64; n]
974 };
975 Self::ppr_with(adjacency, &s, &s)
976 }
977}
978
979fn is_generic_utility(qualified_name: &str, path: &str) -> bool {
986 let mut segs: Vec<String> = Vec::new();
987 for part in path.split('/') {
988 for piece in part.split('.') {
989 if !piece.is_empty() {
990 segs.push(piece.to_lowercase());
991 }
992 }
993 }
994 for part in qualified_name.split(['.', ':']) {
995 if !part.is_empty() {
996 segs.push(part.to_lowercase());
997 }
998 }
999 segs.iter().any(|s| GENERIC_NAMES.contains(&s.as_str()))
1000}
1001
1002fn path_has_test(path: &str) -> bool {
1006 path.split('/').any(|seg| {
1007 let seg = seg.to_lowercase();
1008 seg == "test"
1009 || seg == "tests"
1010 || seg == "spec"
1011 || seg.starts_with("test_")
1012 || seg.ends_with("_test")
1013 })
1014}
1015
1016fn is_generated(path: &str) -> bool {
1020 path.split('/').any(|seg| {
1021 let seg = seg.to_lowercase();
1022 seg.contains("generated")
1023 || seg.starts_with("gen_")
1024 || seg.ends_with("_pb2.py")
1025 || seg.ends_with("_pb.go")
1026 })
1027}
1028
1029fn is_vendored(path: &str) -> bool {
1032 path.split('/').any(|seg| {
1033 matches!(
1034 seg,
1035 "vendor" | "third_party" | "node_modules" | ".venv" | "site-packages" | "bower_components"
1036 )
1037 })
1038}
1039
1040fn ubiquity(view: &TrustedGraphView, symbol_id: &str) -> usize {
1044 let mut sources: HashSet<&str> = HashSet::new();
1045 for r in view.in_edges(symbol_id) {
1046 let tk = view
1047 .entity(&r.object)
1048 .map(|e| e.kind.as_str())
1049 .unwrap_or("");
1050 if reference_kind(&r.predicate, tk).is_some() {
1051 sources.insert(&r.subject);
1052 }
1053 }
1054 sources.len()
1055}
1056
1057pub fn architectural_specificity(entry: &SurfaceEntry, view: &TrustedGraphView) -> f64 {
1065 let mut score: f64 = 1.0;
1066 if entry.exported {
1067 score += 0.15;
1068 }
1069 if !entry.invocation_surfaces.is_empty() {
1070 score += 0.10;
1071 }
1072 if !entry.state_authorities.is_empty() {
1073 score += 0.10;
1074 }
1075 if !entry.contracts.is_empty() {
1076 score += 0.10;
1077 }
1078 if !entry.flows.is_empty() {
1079 score += 0.05;
1080 }
1081 if entry.exported && entry.component.is_some() {
1083 score += 0.05;
1084 }
1085
1086 let mut factor = 1.0;
1087 if is_generic_utility(&entry.qualified_name, &entry.path) {
1088 factor *= 0.5;
1089 }
1090 if path_has_test(&entry.path) {
1091 factor *= 0.5;
1092 }
1093 if is_generated(&entry.path) {
1094 factor *= 0.5;
1095 }
1096 if is_vendored(&entry.path) {
1097 factor *= 0.5;
1098 }
1099 if ubiquity(view, &entry.symbol_id) > UBIQUITY_THRESHOLD {
1100 factor *= 0.5;
1101 }
1102
1103 (score * factor).clamp(0.25, 1.25)
1104}
1105
1106pub const TASK_PPR_WEIGHT: f64 = 0.30;
1112pub const GLOBAL_PPR_WEIGHT: f64 = 0.20;
1114pub const LEXICAL_WEIGHT: f64 = 0.15;
1116pub const SEMANTIC_WEIGHT: f64 = 0.10;
1118pub const CONFIDENCE_WEIGHT: f64 = 0.10;
1120pub const CRITICALITY_WEIGHT: f64 = 0.10;
1122pub const CHANGE_RISK_WEIGHT: f64 = 0.05;
1124pub const NOVELTY_WEIGHT: f64 = 0.05;
1126pub const NO_TASK_GLOBAL_WEIGHT: f64 = 0.50;
1129
1130#[allow(clippy::too_many_arguments)]
1137pub fn final_importance(
1139 task_ppr: f64,
1140 global_ppr: f64,
1141 lexical: f64,
1142 semantic: f64,
1143 confidence: f64,
1144 criticality: f64,
1145 change_risk: f64,
1146 novelty: f64,
1147 has_task: bool,
1148) -> f64 {
1149 let (tw, gw) = if has_task {
1150 (TASK_PPR_WEIGHT, GLOBAL_PPR_WEIGHT)
1151 } else {
1152 (0.0, NO_TASK_GLOBAL_WEIGHT)
1153 };
1154 tw * task_ppr
1155 + gw * global_ppr
1156 + LEXICAL_WEIGHT * lexical
1157 + SEMANTIC_WEIGHT * semantic
1158 + CONFIDENCE_WEIGHT * confidence
1159 + CRITICALITY_WEIGHT * criticality
1160 + CHANGE_RISK_WEIGHT * change_risk
1161 + NOVELTY_WEIGHT * novelty
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167 use scc_core::{entity_id, Entity, Relationship, Visibility};
1168 use scc_graph::{RealityGraph, TrustPolicy};
1169 use scc_store::Store;
1170 use std::collections::HashMap;
1171
1172fn fixture(
1174 entities: Vec<Entity>,
1175 rels: Vec<Relationship>,
1176 ) -> (tempfile::TempDir, Store, RealityGraph) {
1177 let dir = tempfile::TempDir::new().unwrap();
1178 let root = dir.path().join("repo");
1179 std::fs::create_dir_all(&root).unwrap();
1180 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
1181 for e in &entities {
1182 store.insert_entity(e, &["src/fixture.ts".to_string()]).unwrap();
1183 }
1184 for r in &rels {
1185 store.insert_relationship(r, "src/fixture.ts").unwrap();
1186 }
1187 let mut out: HashMap<String, Vec<Relationship>> = HashMap::new();
1188 let mut inn: HashMap<String, Vec<Relationship>> = HashMap::new();
1189 for r in &rels {
1190 out.entry(r.subject.clone()).or_default().push(r.clone());
1191 inn.entry(r.object.clone()).or_default().push(r.clone());
1192 }
1193 let graph = RealityGraph {
1194 repo_id: "r".into(),
1195 entities: entities.into_iter().map(|e| (e.id.clone(), e)).collect(),
1196 out,
1197 inn,
1198 components: vec![],
1199 flows: vec![],
1200 invariants: vec![],
1201 };
1202 (dir, store, graph)
1203 }
1204
1205fn sym(repo: &str, path: &str, name: &str) -> Entity {
1207 let mut e = Entity::new(
1208 scc_core::symbol_id(repo, path, name),
1209 kinds::SYMBOL,
1210 name,
1211 );
1212 e.attr("file", serde_json::json!(path));
1213 e.attr("exported", serde_json::json!(false));
1214 e.attr("start_line", serde_json::json!(1));
1215 e.attr("end_line", serde_json::json!(10));
1216 e
1217 }
1218
1219fn entity(id: &str, kind: &str, name: &str) -> Entity {
1221 Entity::new(id, kind, name)
1222 }
1223
1224fn rel(n: u64, subject: &str, pred: &str, object: &str, prov: Provenance) -> Relationship {
1226 Relationship::new(format!("rel:{n}"), subject, pred, object, prov)
1227 }
1228
1229 #[test]
1232fn reference_normalization_maps_kinds() {
1234 let (a, b, i, c, s, t, m, x, d) = (
1235 scc_core::symbol_id("r", "src/a.ts", "A"),
1236 scc_core::symbol_id("r", "src/b.ts", "B"),
1237 scc_core::symbol_id("r", "src/i.ts", "I"),
1238 "repo://r/contract/c",
1239 "repo://r/state/s",
1240 "repo://r/topic/t",
1241 "repo://r/module/m",
1242 "repo://r/export/x",
1243 "repo://r/annotation/d",
1244 );
1245 let mut a_ent = sym("r", "src/a.ts", "A");
1246 a_ent.attr("file", serde_json::json!("src/a.ts"));
1247 a_ent.attr("start_line", serde_json::json!(10));
1248 a_ent.attr("end_line", serde_json::json!(20));
1249 let b_ent = sym("r", "src/b.ts", "B");
1250 let i_ent = sym("r", "src/i.ts", "I");
1251 let c_ent = entity(c, kinds::CONTRACT, "c");
1252 let s_ent = entity(s, kinds::STATE, "s");
1253 let t_ent = entity(t, kinds::TOPIC, "t");
1254 let m_ent = entity(m, kinds::MODULE, "m");
1255 let x_ent = entity(x, kinds::EXPORT, "x");
1256 let d_ent = entity(d, kinds::ANNOTATION, "d");
1257
1258 let mut rels = vec![
1259 rel(1, &a, predicates::CALLS, &b, Provenance::Extracted),
1260 rel(2, &a, predicates::INVOKES, &b, Provenance::Extracted),
1261 rel(3, &a, predicates::HANDLES_CALLBACK, &b, Provenance::Extracted),
1262 rel(4, &a, predicates::IMPLEMENTS, &i, Provenance::Extracted),
1263 rel(5, &i, predicates::IMPLEMENTED_BY, &a, Provenance::Extracted),
1264 rel(6, &a, predicates::INHERITS, &b, Provenance::Extracted),
1265 rel(7, &a, predicates::REGISTERS, c, Provenance::Extracted),
1266 rel(8, &a, predicates::PUBLISHES, t, Provenance::Extracted),
1267 rel(9, &b, predicates::SUBSCRIBES, t, Provenance::Extracted),
1268 rel(10, &a, predicates::OWNS, s, Provenance::Extracted),
1269 rel(11, &a, predicates::READS, s, Provenance::Extracted),
1270 rel(12, &b, predicates::WRITES, s, Provenance::Extracted),
1271 rel(13, &a, predicates::IMPORTS, m, Provenance::Extracted),
1272 rel(14, &a, predicates::EXPORTS, x, Provenance::Extracted),
1273 rel(15, &a, predicates::DECORATES, d, Provenance::Extracted),
1274 rel(16, &a, predicates::OWNS, c, Provenance::Extracted),
1276 rel(17, &a, predicates::CALLS, d, Provenance::Extracted),
1278 ];
1279 rels[16].confidence = 0.7;
1280 let (_dir, store, graph) = fixture(
1281 vec![a_ent.clone(), b_ent, i_ent, c_ent, s_ent, t_ent, m_ent, x_ent, d_ent],
1282 rels,
1283 );
1284 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1285 let edges = build_reference_graph(&view);
1286
1287 let kinds_between = |src: &str, tgt: &str| -> Vec<ReferenceKind> {
1288 let mut v: Vec<ReferenceKind> = edges
1289 .iter()
1290 .filter(|e| e.source_symbol == src && e.target_symbol == tgt)
1291 .map(|e| e.kind)
1292 .collect();
1293 v.sort();
1294 v
1295 };
1296
1297 assert_eq!(
1300 kinds_between(&a, &b),
1301 vec![
1302 ReferenceKind::Call,
1303 ReferenceKind::Call,
1304 ReferenceKind::Call,
1305 ReferenceKind::Extend
1306 ]
1307 );
1308 assert_eq!(kinds_between(&a, &i), vec![ReferenceKind::Implement; 2]);
1311 assert_eq!(kinds_between(&a, c), vec![ReferenceKind::Register]);
1313 assert_eq!(kinds_between(&a, t), vec![ReferenceKind::Register]);
1314 assert_eq!(kinds_between(&b, t), vec![ReferenceKind::Register]);
1315 let mut as_edges = kinds_between(&a, s);
1317 as_edges.sort();
1318 assert_eq!(as_edges, vec![ReferenceKind::Read, ReferenceKind::Write]);
1319 assert_eq!(kinds_between(&b, s), vec![ReferenceKind::Write]);
1320 assert_eq!(kinds_between(&a, m), vec![ReferenceKind::Import]);
1322 assert_eq!(kinds_between(&a, x), vec![ReferenceKind::Export]);
1323 assert_eq!(kinds_between(&a, d), vec![ReferenceKind::Call, ReferenceKind::Decorate]);
1324 assert!(!kinds_between(&a, c).contains(&ReferenceKind::Write));
1326
1327 let call = edges
1329 .iter()
1330 .find(|e| e.source_symbol == a && e.target_symbol == b && e.kind == ReferenceKind::Call)
1331 .unwrap();
1332 assert!(call.locations.contains(&SourceRange::new("src/a.ts", 10, 20)));
1333 assert!(call.locations.contains(&SourceRange::new("src/b.ts", 1, 10)));
1334
1335 assert_eq!(call.confidence, 1.0);
1337 let low = edges
1338 .iter()
1339 .find(|e| e.source_symbol == a && e.target_symbol == d && e.kind == ReferenceKind::Call)
1340 .unwrap();
1341 assert!((low.confidence - 0.7).abs() < 1e-6);
1342 }
1343
1344 #[test]
1347fn rarity_downweights_ubiquitous() {
1349 let logger = rarity(100, 50);
1352 let distinctive = rarity(100, 1);
1353 assert!(logger < distinctive);
1354 assert!((logger - (100.0_f64 / 51.0).ln()).abs() < 1e-9);
1355
1356 assert_eq!(rarity(100, 10_000), 0.25);
1358 assert_eq!(rarity(100, 0), 1.5);
1359 assert_eq!(rarity(0, 0), 1.0);
1360
1361 let w_common = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 50);
1363 let w_rare = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 1);
1364 assert!(w_common < w_rare);
1365
1366 assert_eq!(edge_weight(predicates::CALLS, Provenance::Stale, 1.0, 100, 1), 0.0);
1369 let resolved = edge_weight(predicates::CALLS, Provenance::Resolved, 1.0, 100, 1);
1370 let extracted = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 1);
1371 assert!(resolved > extracted);
1372 }
1373
1374 #[test]
1377fn utility_pollution_ranks_below_public_api() {
1379 let api_id = scc_core::symbol_id("r", "src/api/order.ts", "OrderApi");
1380 let util_id = scc_core::symbol_id("r", "src/utils/helpers.ts", "helpers");
1381 let mut api_ent = sym("r", "src/api/order.ts", "OrderApi");
1382 api_ent.attr("exported", serde_json::json!(true));
1383 api_ent.attr("entrypoints", serde_json::json!(["http"]));
1384 let util_ent = sym("r", "src/utils/helpers.ts", "helpers");
1385
1386 let mut entities = vec![api_ent.clone(), util_ent.clone()];
1387 let mut rels: Vec<Relationship> = Vec::new();
1388 let mut n = 1u64;
1389 for i in 0..40 {
1391 let caller = scc_core::symbol_id("r", "src/callers/c.rs", &format!("c{i}"));
1392 entities.push(sym("r", "src/callers/c.rs", &format!("c{i}")));
1393 rels.push(rel(
1394 n,
1395 &caller,
1396 predicates::CALLS,
1397 &util_id,
1398 Provenance::Extracted,
1399 ));
1400 n += 1;
1401 }
1402 let c1 = scc_core::symbol_id("r", "src/callers/c.rs", "c0");
1404 let c2 = scc_core::symbol_id("r", "src/callers/c.rs", "c1");
1405 rels.push(rel(n, &c1, predicates::CALLS, &api_id, Provenance::Extracted));
1406 n += 1;
1407 rels.push(rel(n, &c2, predicates::CALLS, &api_id, Provenance::Extracted));
1408
1409 let (_dir, store, graph) = fixture(entities, rels);
1410 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1411 let ranker = SystemRanker::new(&view);
1412 let g = ranker.global_vector();
1413 let util_idx = ranker.index_of(&util_id).unwrap();
1414 let api_idx = ranker.index_of(&api_id).unwrap();
1415
1416 let util_entry = surface_entry(&util_id, "helpers", "src/utils/helpers.ts", false);
1419 let api_entry = surface_entry(&api_id, "OrderApi", "src/api/order.ts", true);
1420 let spec_util = architectural_specificity(&util_entry, &view);
1421 let spec_api = architectural_specificity(&api_entry, &view);
1422 assert_eq!(spec_util, 0.25);
1423 assert_eq!(spec_api, 1.25);
1424 assert!(spec_api > spec_util);
1425
1426 assert!(g[api_idx] * spec_api > g[util_idx] * spec_util);
1429 }
1430
1431 #[test]
1434fn task_personalization_ranks_billing_above_unrelated() {
1436 let billing_client = scc_core::symbol_id("r", "src/billing/client.ts", "BillingClient");
1437 let billing_retry = scc_core::symbol_id("r", "src/billing/client.ts", "BillingClient.retry");
1438 let billing_worker = scc_core::symbol_id("r", "src/billing/worker.ts", "BillingWorker");
1439 let billing_process = scc_core::symbol_id("r", "src/billing/worker.ts", "BillingWorker.process");
1440 let auth_service = scc_core::symbol_id("r", "src/auth/service.ts", "AuthService");
1441 let auth_login = scc_core::symbol_id("r", "src/auth/service.ts", "AuthService.login");
1442 let logger = scc_core::symbol_id("r", "src/logger.ts", "Logger");
1443 let logger_log = scc_core::symbol_id("r", "src/logger.ts", "Logger.log");
1444
1445 let entities = vec![
1446 sym("r", "src/billing/client.ts", "BillingClient"),
1447 sym("r", "src/billing/client.ts", "BillingClient.retry"),
1448 sym("r", "src/billing/worker.ts", "BillingWorker"),
1449 sym("r", "src/billing/worker.ts", "BillingWorker.process"),
1450 sym("r", "src/auth/service.ts", "AuthService"),
1451 sym("r", "src/auth/service.ts", "AuthService.login"),
1452 sym("r", "src/logger.ts", "Logger"),
1453 sym("r", "src/logger.ts", "Logger.log"),
1454 ];
1455 let rels = vec![
1456 rel(1, &billing_client, predicates::CALLS, &billing_retry, Provenance::Extracted),
1457 rel(2, &billing_worker, predicates::CALLS, &billing_process, Provenance::Extracted),
1458 rel(3, &billing_process, predicates::CALLS, &billing_retry, Provenance::Extracted),
1459 rel(4, &auth_service, predicates::CALLS, &auth_login, Provenance::Extracted),
1460 rel(5, &logger, predicates::CALLS, &logger_log, Provenance::Extracted),
1461 ];
1462
1463 let (_dir, store, graph) = fixture(entities, rels);
1464 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1465 let ranker = SystemRanker::new(&view);
1466
1467 let seeds = vec![
1468 TaskSeed {
1469 kind: "symbol".into(),
1470 id: billing_retry.clone(),
1471 weight: 1.0,
1472 },
1473 TaskSeed {
1474 kind: "symbol".into(),
1475 id: billing_process.clone(),
1476 weight: 1.0,
1477 },
1478 ];
1479 let tv = ranker.task_vector(&seeds);
1480 let i_retry = ranker.index_of(&billing_retry).unwrap();
1481 let i_process = ranker.index_of(&billing_process).unwrap();
1482 let i_login = ranker.index_of(&auth_login).unwrap();
1483 let i_log = ranker.index_of(&logger_log).unwrap();
1484
1485 assert!(tv[i_retry] > tv[i_login]);
1487 assert!(tv[i_retry] > tv[i_log]);
1488 assert!(tv[i_process] > tv[i_login]);
1489 assert!(tv[i_process] > tv[i_log]);
1490 assert!(tv[i_retry] > tv[ranker.index_of(&logger).unwrap()]);
1493
1494 let junk = vec![TaskSeed { kind: "symbol".into(), id: "repo://r/symbol/nope/Nope".into(), weight: 1.0 }];
1496 let fallback = ranker.task_vector(&junk);
1497 let g = ranker.global_vector();
1498 assert_eq!(fallback, g);
1499 assert_eq!(fallback.len(), tv.len());
1500 }
1501
1502 fn surface_entry(symbol_id: &str, name: &str, path: &str, exported: bool) -> SurfaceEntry {
1506 let mut entry = SurfaceEntry {
1507 id: symbol_id.to_string(),
1508 symbol_id: symbol_id.to_string(),
1509 qualified_name: name.to_string(),
1510 kind: scc_core::SurfaceKind::Function,
1511 path: path.to_string(),
1512 range: SourceRange::new(path, 1, 1),
1513 source_signature: String::new(),
1514 canonical_signature: String::new(),
1515 semantic_signature: scc_core::SemanticSignature::default(),
1516 visibility: Visibility::Public,
1517 exported,
1518 modifiers: vec![],
1519 annotations: vec![],
1520 component: Some("Order".to_string()),
1521 subsystem: None,
1522 flows: vec![],
1523 contracts: vec![],
1524 state_authorities: vec![],
1525 invocation_surfaces: vec![],
1526 callers: vec![],
1527 callees: vec![],
1528 caller_count: 0,
1529 callee_count: 0,
1530 importance: None,
1531 provenance: Provenance::Extracted,
1532 confidence: 1.0,
1533 rank: scc_core::SurfaceRank::default(),
1534 };
1535 if exported {
1536 entry.invocation_surfaces.push("http".into());
1537 entry.flows.push("f1".into());
1538 entry.contracts.push("c1".into());
1539 }
1540 entry
1541 }
1542
1543 #[test]
1546fn final_importance_blend_constants() {
1548 let v = final_importance(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, true);
1551 assert!((v - 1.05).abs() < 1e-9);
1552
1553 let tasky = final_importance(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true);
1555 assert!((tasky - 0.30).abs() < 1e-9);
1556
1557 let globaly = final_importance(1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false);
1559 assert!((globaly - 0.25).abs() < 1e-9);
1560
1561 let full = final_importance(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, false);
1564 assert!((full - 1.05).abs() < 1e-9);
1565 }
1566
1567 #[test]
1570fn specificity_penalizes_generic_test_and_vendored() {
1572 let (_dir, store, graph) = fixture(vec![sym("r", "src/x.ts", "X")], vec![]);
1573 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1574
1575 let util = surface_entry("s1", "logger", "src/utils/logger.ts", false);
1576 assert_eq!(architectural_specificity(&util, &view), 0.5);
1579
1580 let testy = surface_entry("s2", "TestHelper", "tests/unit/helper_test.rs", false);
1581 assert_eq!(architectural_specificity(&testy, &view), 0.5);
1582
1583 let vendored = surface_entry("s3", "Dep", "vendor/dep/src/lib.rs", false);
1584 assert_eq!(architectural_specificity(&vendored, &view), 0.5);
1585
1586 let generated = surface_entry("s4", "Proto", "src/generated/models.rs", false);
1587 assert_eq!(architectural_specificity(&generated, &view), 0.5);
1588
1589 let facade = surface_entry("s5", "OrderApi", "src/api/order.ts", true);
1591 assert_eq!(architectural_specificity(&facade, &view), 1.25);
1592 }
1593
1594 #[test]
1597fn ranker_universe_is_heterogeneous() {
1599 let owner = scc_core::symbol_id("r", "src/a.ts", "A");
1600 let state_id = entity_id("r", kinds::STATE, "sessions");
1601 let contract_id = entity_id("r", kinds::CONTRACT, "c1");
1602 let topic_id = entity_id("r", kinds::TOPIC, "orders");
1603 let store_id = entity_id("r", kinds::DATA_STORE, "pg");
1604 let flow_id = entity_id("r", kinds::FLOW, "signup");
1605 let file_id = entity_id("r", kinds::FILE, "src/a.ts");
1606
1607 let mut owner_ent = sym("r", "src/a.ts", "A");
1608 owner_ent.attr("exported", serde_json::json!(true));
1609 owner_ent.attr("entrypoints", serde_json::json!(["http"]));
1610 let entities = vec![
1611 owner_ent,
1612 entity(&state_id, kinds::STATE, "sessions"),
1613 entity(&contract_id, kinds::CONTRACT, "c1"),
1614 entity(&topic_id, kinds::TOPIC, "orders"),
1615 entity(&store_id, kinds::DATA_STORE, "pg"),
1616 entity(&flow_id, kinds::FLOW, "signup"),
1617 entity(&file_id, kinds::FILE, "src/a.ts"),
1618 ];
1619 let n = 1u64;
1620 let rels = vec![
1621 rel(n, &owner, predicates::OWNS, &state_id, Provenance::Extracted),
1622 rel(n + 1, &owner, predicates::REGISTERS, &contract_id, Provenance::Extracted),
1623 rel(n + 2, &owner, predicates::PUBLISHES, &topic_id, Provenance::Extracted),
1624 rel(n + 3, &owner, predicates::READS, &store_id, Provenance::Extracted),
1625 rel(n + 4, &owner, predicates::WRITES, &store_id, Provenance::Extracted),
1626 rel(n + 5, &owner, predicates::PARTICIPATES_IN, &flow_id, Provenance::Extracted),
1627 ];
1628
1629 let (_dir, store, graph) = fixture(entities, rels);
1630 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1631 let ranker = SystemRanker::new(&view);
1632
1633 for id in [
1635 owner.as_str(),
1636 state_id.as_str(),
1637 contract_id.as_str(),
1638 topic_id.as_str(),
1639 store_id.as_str(),
1640 flow_id.as_str(),
1641 file_id.as_str(),
1642 ] {
1643 assert!(ranker.index_of(id).is_some(), "{id} must be a node");
1644 }
1645 assert!(ranker.index_of(&entity_id("r", kinds::MODULE, "m")).is_none());
1647
1648 let g = ranker.global_vector();
1650 assert_eq!(g.len(), ranker.nodes().len());
1651
1652 let owner_i = ranker.index_of(&owner).unwrap();
1654 let state_i = ranker.index_of(&state_id).unwrap();
1655 assert!(g[state_i] > 0.0, "state node must receive PPR mass");
1656 assert!(g[owner_i] > 0.0);
1657
1658 let syms = ranker.symbols();
1660 assert_eq!(syms, vec![owner.clone()]);
1661 }
1662
1663 #[test]
1664fn project_to_symbols_lifts_owned_entity_scores() {
1666 let owner = scc_core::symbol_id("r", "src/a.ts", "A");
1667 let other = scc_core::symbol_id("r", "src/b.ts", "B");
1668 let state_id = entity_id("r", kinds::STATE, "sessions");
1669 let contract_id = entity_id("r", kinds::CONTRACT, "c1");
1670
1671 let mut owner_ent = sym("r", "src/a.ts", "A");
1672 owner_ent.attr("exported", serde_json::json!(true));
1673 owner_ent.attr("entrypoints", serde_json::json!(["http"]));
1674 let entities = vec![
1675 owner_ent,
1676 sym("r", "src/b.ts", "B"),
1677 entity(&state_id, kinds::STATE, "sessions"),
1678 entity(&contract_id, kinds::CONTRACT, "c1"),
1679 ];
1680 let rels = vec![
1681 rel(1, &owner, predicates::OWNS, &state_id, Provenance::Extracted),
1682 rel(2, &owner, predicates::REGISTERS, &contract_id, Provenance::Extracted),
1683 rel(3, &other, predicates::OWNS, &state_id, Provenance::Extracted),
1685 ];
1686 let (_dir, store, graph) = fixture(entities, rels);
1687 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1688 let ranker = SystemRanker::new(&view);
1689
1690 let g = ranker.global_vector();
1691 let projected = ranker.project_to_symbols(&g);
1692 let score_of = |id: &str| -> f64 {
1693 projected
1694 .iter()
1695 .find(|(s, _)| s == id)
1696 .map(|(_, v)| *v)
1697 .unwrap_or(f64::NAN)
1698 };
1699
1700 let own_a = score_of(&owner);
1701 let own_b = score_of(&other);
1702 let state_score = g[ranker.index_of(&state_id).unwrap()];
1703 let contract_score = g[ranker.index_of(&contract_id).unwrap()];
1704
1705 let bonus_a = (0.4 * (state_score + contract_score)).min(0.5);
1708 let bonus_b = (0.4 * state_score).min(0.5);
1709 assert!((own_a - (g[ranker.index_of(&owner).unwrap()] + bonus_a)).abs() < 1e-9);
1710 assert!((own_b - (g[ranker.index_of(&other).unwrap()] + bonus_b)).abs() < 1e-9);
1711 assert!(own_a > own_b);
1713
1714 let mut big_v = vec![0.0; g.len()];
1716 big_v[ranker.index_of(&state_id).unwrap()] = 100.0;
1717 big_v[ranker.index_of(&contract_id).unwrap()] = 100.0;
1718 let capped = ranker.project_to_symbols(&big_v);
1719 let cap_a = capped.iter().find(|(s, _)| s == &owner).unwrap().1;
1720 let own_a0 = big_v[ranker.index_of(&owner).unwrap()];
1721 assert!((cap_a - (own_a0 + 0.5)).abs() < 1e-9);
1722
1723 let again = ranker.project_to_symbols(&g);
1725 assert_eq!(again, projected);
1726 }
1727
1728 #[test]
1731fn rank_edge_kind_direction_and_weights() {
1733 for (container, member) in [
1744 (kinds::SYSTEM, kinds::SUBSYSTEM),
1745 (kinds::SUBSYSTEM, kinds::SERVICE),
1746 (kinds::SUBSYSTEM, kinds::COMPONENT),
1747 (kinds::SERVICE, kinds::COMPONENT),
1748 (kinds::COMPONENT, kinds::FILE),
1749 (kinds::COMPONENT, kinds::SYMBOL),
1750 (kinds::FILE, kinds::SYMBOL),
1751 (kinds::SYSTEM, kinds::SERVICE),
1754 (kinds::SYSTEM, kinds::FILE),
1755 (kinds::SUBSYSTEM, kinds::SYMBOL),
1756 (kinds::SERVICE, kinds::FILE),
1757 ] {
1758 assert_eq!(
1759 rank_edge_kind(predicates::CONTAINS, container, member),
1760 Some(RankEdgeKind::Contains)
1761 );
1762 assert_eq!(
1763 rank_edge_kind(predicates::CONTAINS, member, container),
1764 Some(RankEdgeKind::MemberOf)
1765 );
1766 }
1767 assert_eq!(
1769 rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::COMPONENT),
1770 Some(RankEdgeKind::MemberOf)
1771 );
1772 assert_eq!(
1773 rank_edge_kind(predicates::PARTICIPATES_IN, kinds::SYMBOL, kinds::FLOW),
1774 Some(RankEdgeKind::ParticipatesIn)
1775 );
1776 assert_eq!(
1777 rank_edge_kind(predicates::PARTICIPATES_IN, kinds::FLOW, kinds::SYMBOL),
1778 Some(RankEdgeKind::FlowReachesParticipant)
1779 );
1780 assert_eq!(
1781 rank_edge_kind(predicates::HANDLES, kinds::SYMBOL, kinds::ROUTE),
1782 Some(RankEdgeKind::Handles)
1783 );
1784 assert_eq!(
1785 rank_edge_kind(predicates::DEFINES, kinds::SYMBOL, kinds::SCHEMA),
1786 Some(RankEdgeKind::Defines)
1787 );
1788 assert_eq!(
1791 rank_edge_kind(predicates::HANDLES, kinds::ROUTE, kinds::SYMBOL),
1792 Some(RankEdgeKind::HandledBy)
1793 );
1794 assert_eq!(
1795 rank_edge_kind(predicates::DEFINES, kinds::SCHEMA, kinds::SYMBOL),
1796 Some(RankEdgeKind::DefinedBy)
1797 );
1798
1799 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::COMPONENT, kinds::FLOW), None);
1801 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::CONTRACT), None);
1802 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::FLOW), None);
1803 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::COMPONENT, kinds::COMPONENT), None);
1805 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::SYMBOL), None);
1806 assert_eq!(rank_edge_kind(predicates::PARTICIPATES_IN, kinds::SYMBOL, kinds::CONTRACT), None);
1807 assert_eq!(rank_edge_kind(predicates::HANDLES, kinds::SYMBOL, kinds::ENDPOINT), None);
1808 assert_eq!(rank_edge_kind(predicates::DEFINES, kinds::SYMBOL, kinds::CONTRACT), None);
1809 assert_eq!(rank_edge_kind(predicates::CALLS, kinds::SYMBOL, kinds::SYMBOL), None);
1810
1811 assert_eq!(RankEdgeKind::Contains.weight(), RANK_MEMBERSHIP);
1813 assert_eq!(RankEdgeKind::MemberOf.weight(), RANK_MEMBER_OF);
1814 assert_eq!(RankEdgeKind::ParticipatesIn.weight(), RANK_PARTICIPATES);
1815 assert_eq!(RankEdgeKind::FlowReachesParticipant.weight(), RANK_FLOW_REACHES_PARTICIPANT);
1816 assert_eq!(RankEdgeKind::Handles.weight(), RANK_HANDLES);
1817 assert_eq!(RankEdgeKind::HandledBy.weight(), RANK_HANDLED_BY);
1818 assert_eq!(RankEdgeKind::Defines.weight(), RANK_DEFINES);
1819 assert_eq!(RankEdgeKind::DefinedBy.weight(), RANK_DEFINED_BY);
1820
1821 assert_eq!(RankEdgeKind::Contains.reverse_ranking_transition(), Some(RankEdgeKind::MemberOf));
1827 assert_eq!(RankEdgeKind::MemberOf.reverse_ranking_transition(), Some(RankEdgeKind::Contains));
1828 assert_eq!(RankEdgeKind::ParticipatesIn.reverse_ranking_transition(), Some(RankEdgeKind::FlowReachesParticipant));
1829 assert_eq!(RankEdgeKind::FlowReachesParticipant.reverse_ranking_transition(), Some(RankEdgeKind::ParticipatesIn));
1830 assert_eq!(RankEdgeKind::Handles.reverse_ranking_transition(), Some(RankEdgeKind::HandledBy));
1831 assert_eq!(RankEdgeKind::HandledBy.reverse_ranking_transition(), Some(RankEdgeKind::Handles));
1832 assert_eq!(RankEdgeKind::Defines.reverse_ranking_transition(), Some(RankEdgeKind::DefinedBy));
1833 assert_eq!(RankEdgeKind::DefinedBy.reverse_ranking_transition(), Some(RankEdgeKind::Defines));
1834 }
1835
1836 #[test]
1837fn contains_edges_spread_component_importance_both_ways() {
1839 let comp_id = entity_id("r", kinds::COMPONENT, "orders");
1845 let m1 = scc_core::symbol_id("r", "src/orders/a.ts", "OrderService");
1846 let m2 = scc_core::symbol_id("r", "src/orders/b.ts", "OrderRepo");
1847 let x = scc_core::symbol_id("r", "src/other.ts", "Unrelated");
1848
1849 let entities = vec![
1850 entity(&comp_id, kinds::COMPONENT, "orders"),
1851 sym("r", "src/orders/a.ts", "OrderService"),
1852 sym("r", "src/orders/b.ts", "OrderRepo"),
1853 sym("r", "src/other.ts", "Unrelated"),
1854 ];
1855 let rels = vec![
1856 rel(1, &comp_id, predicates::CONTAINS, &m1, Provenance::Extracted),
1857 rel(2, &comp_id, predicates::CONTAINS, &m2, Provenance::Extracted),
1858 ];
1859 let (_dir, store, graph) = fixture(entities, rels);
1860 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1861 let ranker = SystemRanker::new(&view);
1862 let g = ranker.global_vector();
1863
1864 let c = g[ranker.index_of(&comp_id).unwrap()];
1865 let s1 = g[ranker.index_of(&m1).unwrap()];
1866 let s2 = g[ranker.index_of(&m2).unwrap()];
1867 let sx = g[ranker.index_of(&x).unwrap()];
1868
1869 assert!(c > 0.3, "component importance derived from members: {c}");
1875 assert!(s1 > sx && s2 > sx, "members lifted above the isolated symbol");
1876 assert!(c > s1 && c > s2, "component spreads importance down to members");
1877 }
1878
1879 #[test]
1880fn participates_in_connects_flow_nodes() {
1882 let s = scc_core::symbol_id("r", "src/a.ts", "A");
1888 let f1 = entity_id("r", kinds::FLOW, "signup");
1889 let f2 = entity_id("r", kinds::FLOW, "checkout");
1890
1891 let entities = vec![
1892 sym("r", "src/a.ts", "A"),
1893 entity(&f1, kinds::FLOW, "signup"),
1894 entity(&f2, kinds::FLOW, "checkout"),
1895 ];
1896 let rels = vec![rel(1, &s, predicates::PARTICIPATES_IN, &f1, Provenance::Extracted)];
1897 let (_dir, store, graph) = fixture(entities, rels);
1898 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1899 let ranker = SystemRanker::new(&view);
1900 let g = ranker.global_vector();
1901
1902 let f1v = g[ranker.index_of(&f1).unwrap()];
1903 let f2v = g[ranker.index_of(&f2).unwrap()];
1904 let sv = g[ranker.index_of(&s).unwrap()];
1905
1906 assert!(f1v > 0.3, "flow with a participant receives PPR mass: {f1v}");
1910 assert!(f1v > f2v, "connected flow ranks above the bare flow");
1911 assert!(sv > f2v, "flow importance reaches the participant: {sv} vs {f2v}");
1913 }
1914
1915 #[test]
1916fn handles_and_defines_edges_feed_ppr() {
1918 let h = scc_core::symbol_id("r", "src/api.ts", "OrderHandler");
1927 let route = entity_id("r", kinds::ROUTE, "/orders");
1928 let d = scc_core::symbol_id("r", "src/models.ts", "Order");
1929 let schema = entity_id("r", kinds::SCHEMA, "order");
1930 let x = scc_core::symbol_id("r", "src/util.ts", "Helper");
1931
1932 let mut d_ent = sym("r", "src/models.ts", "Order");
1933 d_ent.attr("exported", serde_json::json!(true));
1934 let entities = vec![
1935 sym("r", "src/api.ts", "OrderHandler"),
1936 entity(&route, kinds::ROUTE, "/orders"),
1937 d_ent,
1938 entity(&schema, kinds::SCHEMA, "order"),
1939 sym("r", "src/util.ts", "Helper"),
1940 ];
1941 let rels = vec![
1942 rel(1, &h, predicates::HANDLES, &route, Provenance::Extracted),
1943 rel(2, &d, predicates::DEFINES, &schema, Provenance::Extracted),
1944 ];
1945 let (_dir, store, graph) = fixture(entities, rels);
1946 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1947 let ranker = SystemRanker::new(&view);
1948 let g = ranker.global_vector();
1949
1950 let rv = g[ranker.index_of(&route).unwrap()];
1951 let sv = g[ranker.index_of(&schema).unwrap()];
1952 let xv = g[ranker.index_of(&x).unwrap()];
1953
1954 assert!(rv > xv, "route receives mass via the HANDLES edge: {rv} vs {xv}");
1955 assert!(sv > xv, "schema receives mass via the DEFINES edge: {sv} vs {xv}");
1956 }
1957
1958 #[test]
1961fn file_contains_symbol_lifts_member() {
1963 let file_id = entity_id("r", kinds::FILE, "src/orders.ts");
1971 let sym_id = scc_core::symbol_id("r", "src/orders.ts", "OrderService");
1972 let x = scc_core::symbol_id("r", "src/other.ts", "Unrelated");
1973
1974 let entities = vec![
1975 entity(&file_id, kinds::FILE, "src/orders.ts"),
1976 sym("r", "src/orders.ts", "OrderService"),
1977 sym("r", "src/other.ts", "Unrelated"),
1978 ];
1979 let rels = vec![rel(1, &file_id, predicates::CONTAINS, &sym_id, Provenance::Extracted)];
1980 let (_dir, store, graph) = fixture(entities, rels);
1981 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1982 let ranker = SystemRanker::new(&view);
1983 let g = ranker.global_vector();
1984
1985 let fv = g[ranker.index_of(&file_id).unwrap()];
1986 let sv = g[ranker.index_of(&sym_id).unwrap()];
1987 let xv = g[ranker.index_of(&x).unwrap()];
1988
1989 assert!(fv > 0.3, "file node derives importance from its symbol: {fv}");
1993 assert!(sv > xv, "contained symbol lifted above the isolated one: {sv} vs {xv}");
1994 assert!(fv > xv, "file lifted above the isolated symbol: {fv} vs {xv}");
1995 }
1996
1997 #[test]
1998fn subsystem_component_containment_spreads_both_ways() {
2000 let sub_id = entity_id("r", kinds::SUBSYSTEM, "billing");
2006 let comp_id = entity_id("r", kinds::COMPONENT, "orders");
2007 let bare = entity_id("r", kinds::COMPONENT, "auth");
2008
2009 let entities = vec![
2010 entity(&sub_id, kinds::SUBSYSTEM, "billing"),
2011 entity(&comp_id, kinds::COMPONENT, "orders"),
2012 entity(&bare, kinds::COMPONENT, "auth"),
2013 ];
2014 let rels = vec![rel(1, &sub_id, predicates::CONTAINS, &comp_id, Provenance::Extracted)];
2015 let (_dir, store, graph) = fixture(entities, rels);
2016 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2017 let ranker = SystemRanker::new(&view);
2018 let g = ranker.global_vector();
2019
2020 let subv = g[ranker.index_of(&sub_id).unwrap()];
2021 let compv = g[ranker.index_of(&comp_id).unwrap()];
2022 let barev = g[ranker.index_of(&bare).unwrap()];
2023
2024 assert!(compv > barev, "contained component ranks above the bare one: {compv} vs {barev}");
2028 assert!(subv > barev, "subsystem derives importance from its component: {subv} vs {barev}");
2029 assert!(subv > 0.3, "subsystem is a first-class rank node: {subv}");
2030 }
2031
2032 #[test]
2035fn hot_route_projects_to_handler_symbol() {
2037 let handler = scc_core::symbol_id("r", "src/router.ts", "build_router");
2044 let route = entity_id("r", kinds::ROUTE, "/health");
2045 let other = scc_core::symbol_id("r", "src/util.ts", "Helper");
2046
2047 let entities = vec![
2048 sym("r", "src/router.ts", "build_router"),
2049 entity(&route, kinds::ROUTE, "/health"),
2050 sym("r", "src/util.ts", "Helper"),
2051 ];
2052 let rels = vec![rel(1, &handler, predicates::HANDLES, &route, Provenance::Extracted)];
2053 let (_dir, store, graph) = fixture(entities, rels);
2054 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2055 let ranker = SystemRanker::new(&view);
2056
2057 let seeds = vec![TaskSeed {
2059 kind: "route".into(),
2060 id: route.clone(),
2061 weight: 1.0,
2062 }];
2063 let tv = ranker.task_vector(&seeds);
2064 let projected = ranker.project_to_symbols(&tv);
2065 let score_of = |id: &str| -> f64 {
2066 projected
2067 .iter()
2068 .find(|(s, _)| s == id)
2069 .map(|(_, v)| *v)
2070 .unwrap_or(f64::NAN)
2071 };
2072
2073 let route_mass = tv[ranker.index_of(&route).unwrap()];
2074 let handler_own = tv[ranker.index_of(&handler).unwrap()];
2075 let handler_score = score_of(&handler);
2076 let other_score = score_of(&other);
2077
2078 let expected_bonus = (PROJECTION_BONUS_FACTOR * route_mass).min(PROJECTION_BONUS_CAP);
2082 assert!((handler_score - (handler_own + expected_bonus)).abs() < 1e-9);
2083 assert!(route_mass > 0.1, "seeded route is hot: {route_mass}");
2084 assert!(handler_score > other_score, "hot route lifts its handler above unrelated symbols");
2085 }
2086
2087 #[test]
2088fn hot_schema_projects_to_definer_symbol() {
2090 let definer = scc_core::symbol_id("r", "src/models.ts", "Order");
2095 let schema = entity_id("r", kinds::SCHEMA, "order");
2096 let other = scc_core::symbol_id("r", "src/util.ts", "Helper");
2097
2098 let entities = vec![
2099 sym("r", "src/models.ts", "Order"),
2100 entity(&schema, kinds::SCHEMA, "order"),
2101 sym("r", "src/util.ts", "Helper"),
2102 ];
2103 let rels = vec![rel(1, &definer, predicates::DEFINES, &schema, Provenance::Extracted)];
2104 let (_dir, store, graph) = fixture(entities, rels);
2105 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2106 let ranker = SystemRanker::new(&view);
2107
2108 let seeds = vec![TaskSeed {
2109 kind: "schema".into(),
2110 id: schema.clone(),
2111 weight: 1.0,
2112 }];
2113 let tv = ranker.task_vector(&seeds);
2114 let projected = ranker.project_to_symbols(&tv);
2115 let score_of = |id: &str| -> f64 {
2116 projected
2117 .iter()
2118 .find(|(s, _)| s == id)
2119 .map(|(_, v)| *v)
2120 .unwrap_or(f64::NAN)
2121 };
2122
2123 let schema_mass = tv[ranker.index_of(&schema).unwrap()];
2124 let definer_own = tv[ranker.index_of(&definer).unwrap()];
2125 let definer_score = score_of(&definer);
2126 let other_score = score_of(&other);
2127
2128 let expected_bonus = (PROJECTION_BONUS_FACTOR * schema_mass).min(PROJECTION_BONUS_CAP);
2129 assert!((definer_score - (definer_own + expected_bonus)).abs() < 1e-9);
2130 assert!(schema_mass > 0.1, "seeded schema is hot: {schema_mass}");
2131 assert!(definer_score > other_score, "hot schema lifts its definer above unrelated symbols");
2132 }
2133}