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 edge_list: Vec<(usize, usize, String, f64)>,
580 in_degree: Vec<usize>,
582 projection: Vec<Vec<usize>>,
587 global: Vec<f64>,
589}
590
591impl<'a> SystemRanker<'a> {
593 pub fn new(view: &'a TrustedGraphView<'a>) -> SystemRanker<'a> {
596 Self::with_edge_adjust(view, no_adjust)
597 }
598
599 pub fn with_edge_adjust<F>(
606 view: &'a TrustedGraphView<'a>,
607 adjust: F,
608 ) -> SystemRanker<'a>
609 where
610 F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
611 {
612 Self::with_edge_adjust_and_extra(view, adjust, &[])
613 }
614
615 pub fn with_edge_adjust_and_extra<F>(
624 view: &'a TrustedGraphView<'a>,
625 adjust: F,
626 extra: &[(String, String, String, f64)],
627 ) -> SystemRanker<'a>
628 where
629 F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
630 {
631 Self::with_edge_adjust_extra_and_nodes(view, adjust, extra, &[])
632 }
633
634 pub fn with_edge_adjust_extra_and_nodes<F>(
643 view: &'a TrustedGraphView<'a>,
644 adjust: F,
645 extra: &[(String, String, String, f64)],
646 extra_nodes: &[(String, String)],
647 ) -> SystemRanker<'a>
648 where
649 F: for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
650 {
651 let mut pairs: Vec<(String, String)> = view
652 .entities()
653 .filter(|e| RANKABLE_KINDS.contains(&e.kind.as_str()))
654 .map(|e| (e.id.clone(), e.kind.clone()))
655 .collect();
656 for (id, kind) in extra_nodes {
657 if id.is_empty() || !RANKABLE_KINDS.contains(&kind.as_str()) {
658 continue;
659 }
660 if pairs.iter().any(|(eid, _)| eid == id) {
661 continue;
662 }
663 pairs.push((id.clone(), kind.clone()));
664 }
665 pairs.sort_by(|a, b| a.0.cmp(&b.0));
666 let nodes: Vec<String> = pairs.iter().map(|(id, _)| id.clone()).collect();
667 let kinds: Vec<String> = pairs.iter().map(|(_, k)| k.clone()).collect();
668 let index: HashMap<String, usize> = nodes
669 .iter()
670 .enumerate()
671 .map(|(i, s)| (s.clone(), i))
672 .collect();
673 let n = nodes.len();
674
675 let mut edges: Vec<(usize, usize, String, f64)> = Vec::new();
682 let mut in_sources: Vec<HashSet<usize>> = vec![HashSet::new(); n];
683 for rel in view.all_rels() {
684 let (Some(&si), Some(&ti)) = (index.get(&rel.subject), index.get(&rel.object)) else {
685 continue;
686 };
687 let subject_kind = view
688 .entity(&rel.subject)
689 .map(|e| e.kind.as_str())
690 .unwrap_or("");
691 let target_kind = view
692 .entity(&rel.object)
693 .map(|e| e.kind.as_str())
694 .unwrap_or("");
695 let reference_weight = if reference_kind(&rel.predicate, target_kind).is_some() {
698 Some(
699 predicate_weight(&rel.predicate, rel.provenance)
700 * provenance_weight(rel.provenance)
701 * rel.confidence.clamp(0.0, 1.0),
702 )
703 } else {
704 None
705 };
706 if let Some(kind) = rank_edge_kind(&rel.predicate, subject_kind, target_kind) {
710 let w = kind.weight()
711 * provenance_weight(rel.provenance)
712 * rel.confidence.clamp(0.0, 1.0);
713 if let Some(w) = Self::adjust_edge(&adjust, &rel.subject, &rel.predicate, &rel.object, w) {
714 in_sources[ti].insert(si);
715 edges.push((si, ti, rel.predicate.clone(), w));
716 }
717 if let Some(rev) = kind.reverse_ranking_transition() {
718 let rw = rev.weight()
719 * provenance_weight(rel.provenance)
720 * rel.confidence.clamp(0.0, 1.0);
721 if let Some(rw) = Self::adjust_edge(&adjust, &rel.object, &rel.predicate, &rel.subject, rw) {
722 in_sources[si].insert(ti);
723 edges.push((ti, si, rel.predicate.clone(), rw));
724 }
725 }
726 }
727 if let Some(w) = reference_weight {
728 if let Some(w) = Self::adjust_edge(&adjust, &rel.subject, &rel.predicate, &rel.object, w) {
729 in_sources[ti].insert(si);
730 edges.push((si, ti, rel.predicate.clone(), w));
731 }
732 }
733 }
734 for (subj, pred, obj, w) in extra {
735 if !w.is_finite() || *w <= 0.0 {
736 continue;
737 }
738 let (Some(&si), Some(&ti)) = (index.get(subj), index.get(obj)) else {
739 continue;
740 };
741 if let Some(w) = Self::adjust_edge(&adjust, subj, pred, obj, *w) {
742 in_sources[ti].insert(si);
743 edges.push((si, ti, pred.clone(), w));
744 }
745 }
746 let in_degree: Vec<usize> = in_sources.iter().map(|s| s.len()).collect();
747
748 let edge_list: Vec<(usize, usize, String, f64)> = edges.to_vec();
751 let mut agg: Vec<HashMap<usize, f64>> = vec![HashMap::new(); n];
753 for (si, ti, _pred, w) in edges.iter() {
754 let (si, ti, w) = (*si, *ti, *w);
755 let r = rarity(n, in_degree[ti]);
756 *agg[si].entry(ti).or_insert(0.0) += w * r;
757 }
758 let mut adjacency: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
759 for (i, row) in agg.iter().enumerate() {
760 let sum: f64 = row.values().sum();
761 if sum > 0.0 {
762 let mut v: Vec<(usize, f64)> =
763 row.iter().map(|(j, w)| (*j, w / sum)).collect();
764 v.sort_by_key(|(j, _)| *j);
765 adjacency[i] = v;
766 }
767 }
768
769 let mut projection: Vec<Vec<usize>> = vec![Vec::new(); n];
774 for rel in view.all_rels() {
775 if !PROJECTION_PREDICATES.contains(&rel.predicate.as_str()) {
776 continue;
777 }
778 let (Some(&si), Some(&ti)) = (index.get(&rel.subject), index.get(&rel.object)) else {
779 continue;
780 };
781 if kinds[ti] == kinds::SYMBOL {
782 continue; }
784 projection[si].push(ti);
785 }
786 for p in projection.iter_mut() {
787 p.sort_unstable();
788 p.dedup();
789 }
790
791 let ranker = SystemRanker {
792 view,
793 nodes,
794 kinds,
795 index,
796 adjacency,
797 in_degree,
798 projection,
799 edge_list,
800 global: Vec::new(),
801 };
802 let global = Self::ppr(&ranker.adjacency, &ranker.global_personalization());
803 let mut ranker = ranker;
804 ranker.global = global;
805 ranker
806 }
807
808 fn adjust_edge(
812 adjust: &impl for<'x, 'y, 'z> Fn(&'x str, &'y str, &'z str, f64) -> Option<(String, f64)>,
813 subject: &str,
814 predicate: &str,
815 object: &str,
816 base: f64,
817 ) -> Option<f64> {
818 match adjust(subject, predicate, object, base) {
819 None => Some(base),
820 Some((mode, value)) => match mode.as_str() {
821 "add" => Some(base + value),
822 "multiply" => Some(base * value),
823 "replace" => Some(value),
824 "veto" => None,
825 _ => Some(base),
826 }
827 .filter(|w| w.is_finite() && *w > 0.0),
828 }
829 }
830
831 pub fn nodes(&self) -> &[String] {
835 &self.nodes
836 }
837
838 pub fn kinds(&self) -> &[String] {
842 &self.kinds
843 }
844
845 pub fn rank_edges(&self) -> Vec<(String, String, String, f64)> {
850 self.edge_list
851 .iter()
852 .map(|(si, ti, pred, w)| (self.nodes[*si].clone(), pred.clone(), self.nodes[*ti].clone(), *w))
853 .collect()
854 }
855
856 pub fn symbols(&self) -> Vec<String> {
861 self.nodes
862 .iter()
863 .zip(self.kinds.iter())
864 .filter(|(_, k)| k.as_str() == kinds::SYMBOL)
865 .map(|(id, _)| id.clone())
866 .collect()
867 }
868
869 pub fn index_of(&self, id: &str) -> Option<usize> {
872 self.index.get(id).copied()
873 }
874
875 pub fn in_degree(&self, id: &str) -> usize {
878 self.index
879 .get(id)
880 .map(|i| self.in_degree[*i])
881 .unwrap_or(0)
882 }
883
884 pub fn global_vector(&self) -> Vec<f64> {
889 self.global.clone()
890 }
891
892 pub fn project_to_symbols(&self, vector: &[f64]) -> Vec<(String, f64)> {
904 let mut scores: Vec<f64> = Vec::with_capacity(self.nodes.len());
905 for (i, _id) in self.nodes.iter().enumerate() {
906 let own = vector.get(i).copied().unwrap_or(0.0);
907 let mut bonus = 0.0;
908 for &j in self.projection[i].iter() {
909 bonus += vector.get(j).copied().unwrap_or(0.0);
910 }
911 scores.push(own + (PROJECTION_BONUS_FACTOR * bonus).min(PROJECTION_BONUS_CAP));
912 }
913 let mut out: Vec<(String, f64)> = Vec::new();
914 for (i, id) in self.nodes.iter().enumerate() {
915 if self.kinds[i] == kinds::SYMBOL {
916 out.push((id.clone(), scores[i]));
917 }
918 }
919 out
920 }
921
922 pub fn task_vector(&self, seeds: &[TaskSeed]) -> Vec<f64> {
929 let n = self.nodes.len();
930 if n == 0 {
931 return Vec::new();
932 }
933 let mut s = vec![0.0; n];
934 let mut found = false;
935 for seed in seeds {
936 if let Some(&i) = self.index.get(&seed.id) {
937 s[i] += seed.weight.max(0.0);
938 found = true;
939 }
940 }
941 if !found {
942 return self.global.clone();
943 }
944 let sum: f64 = s.iter().sum();
945 if sum <= 0.0 {
946 return self.global.clone();
947 }
948 let s: Vec<f64> = s.iter().map(|v| v / sum).collect();
949 let mut start = vec![0.0; n];
950 for i in 0..n {
951 start[i] = WARM_START_GLOBAL_BLEND * self.global[i] + (1.0 - WARM_START_GLOBAL_BLEND) * s[i];
952 }
953 Self::ppr_with(&self.adjacency, &s, &start)
954 }
955
956 fn global_personalization(&self) -> Vec<f64> {
963 let n = self.nodes.len();
964 let mut seeds = vec![0.0; n];
965 let mut flow_eps: HashSet<String> = HashSet::new();
967 for f in self.view.flows() {
968 if let Some(ep) = f.attributes.get("entrypoint").and_then(|v| v.as_str()) {
969 flow_eps.insert(ep.to_string());
970 }
971 }
972 for (i, id) in self.nodes.iter().enumerate() {
973 let mut seed = false;
974 if let Some(e) = self.view.entity(id) {
975 let exported = e
976 .attributes
977 .get("exported")
978 .and_then(|v| v.as_bool())
979 .unwrap_or(false);
980 let has_entrypoints = e
981 .attributes
982 .get("entrypoints")
983 .and_then(|v| v.as_array())
984 .map(|a| !a.is_empty())
985 .unwrap_or(false);
986 seed = seed || exported || has_entrypoints;
987 }
988 if flow_eps.contains(id.as_str()) {
989 seed = true;
990 }
991 for r in self.view.out_edges(id) {
992 let tk = self
993 .view
994 .entity(&r.object)
995 .map(|e| e.kind.as_str())
996 .unwrap_or("");
997 match r.predicate.as_str() {
998 predicates::HANDLES if INVOCATION_KINDS.contains(&tk) => seed = true,
999 predicates::OWNS if tk == kinds::STATE || tk == kinds::REACTIVE => seed = true,
1000 predicates::REGISTERS | predicates::PUBLISHES
1001 if CONTRACT_KINDS.contains(&tk) =>
1002 {
1003 seed = true;
1004 }
1005 _ => {}
1006 }
1007 }
1008 if seed {
1009 seeds[i] = 1.0;
1010 }
1011 }
1012 seeds
1013 }
1014
1015 fn ppr_with(adjacency: &[Vec<(usize, f64)>], personalization: &[f64], start: &[f64]) -> Vec<f64> {
1019 let n = adjacency.len();
1020 if n == 0 {
1021 return Vec::new();
1022 }
1023 let mut r = start.to_vec();
1024 let d = DAMPING_FACTOR;
1025 for _ in 0..POWER_ITERATIONS {
1026 let mut nr = vec![0.0; n];
1027 let mut dangling = 0.0;
1028 for (i, row) in adjacency.iter().enumerate() {
1029 let ri = r[i];
1030 if row.is_empty() {
1031 dangling += ri;
1032 } else {
1033 for (j, w) in row {
1034 nr[*j] += ri * w;
1035 }
1036 }
1037 }
1038 let d_mass = d * dangling / n as f64;
1039 for k in 0..n {
1040 nr[k] = (1.0 - d) * personalization[k] + d * nr[k] + d_mass;
1041 }
1042 r = nr;
1043 }
1044 r
1045 }
1046
1047 fn ppr(adjacency: &[Vec<(usize, f64)>], seeds: &[f64]) -> Vec<f64> {
1051 let n = adjacency.len();
1052 if n == 0 {
1053 return Vec::new();
1054 }
1055 let sum: f64 = seeds.iter().sum();
1056 let s: Vec<f64> = if sum > 0.0 {
1057 seeds.iter().map(|v| v / sum).collect()
1058 } else {
1059 vec![1.0 / n as f64; n]
1060 };
1061 Self::ppr_with(adjacency, &s, &s)
1062 }
1063}
1064
1065fn is_generic_utility(qualified_name: &str, path: &str) -> bool {
1072 let mut segs: Vec<String> = Vec::new();
1073 for part in path.split('/') {
1074 for piece in part.split('.') {
1075 if !piece.is_empty() {
1076 segs.push(piece.to_lowercase());
1077 }
1078 }
1079 }
1080 for part in qualified_name.split(['.', ':']) {
1081 if !part.is_empty() {
1082 segs.push(part.to_lowercase());
1083 }
1084 }
1085 segs.iter().any(|s| GENERIC_NAMES.contains(&s.as_str()))
1086}
1087
1088fn path_has_test(path: &str) -> bool {
1092 path.split('/').any(|seg| {
1093 let seg = seg.to_lowercase();
1094 seg == "test"
1095 || seg == "tests"
1096 || seg == "spec"
1097 || seg.starts_with("test_")
1098 || seg.ends_with("_test")
1099 })
1100}
1101
1102fn is_generated(path: &str) -> bool {
1106 path.split('/').any(|seg| {
1107 let seg = seg.to_lowercase();
1108 seg.contains("generated")
1109 || seg.starts_with("gen_")
1110 || seg.ends_with("_pb2.py")
1111 || seg.ends_with("_pb.go")
1112 })
1113}
1114
1115fn is_vendored(path: &str) -> bool {
1118 path.split('/').any(|seg| {
1119 matches!(
1120 seg,
1121 "vendor" | "third_party" | "node_modules" | ".venv" | "site-packages" | "bower_components"
1122 )
1123 })
1124}
1125
1126fn ubiquity(view: &TrustedGraphView, symbol_id: &str) -> usize {
1130 let mut sources: HashSet<&str> = HashSet::new();
1131 for r in view.in_edges(symbol_id) {
1132 let tk = view
1133 .entity(&r.object)
1134 .map(|e| e.kind.as_str())
1135 .unwrap_or("");
1136 if reference_kind(&r.predicate, tk).is_some() {
1137 sources.insert(&r.subject);
1138 }
1139 }
1140 sources.len()
1141}
1142
1143pub fn architectural_specificity(entry: &SurfaceEntry, view: &TrustedGraphView) -> f64 {
1151 let mut score: f64 = 1.0;
1152 if entry.exported {
1153 score += 0.15;
1154 }
1155 if !entry.invocation_surfaces.is_empty() {
1156 score += 0.10;
1157 }
1158 if !entry.state_authorities.is_empty() {
1159 score += 0.10;
1160 }
1161 if !entry.contracts.is_empty() {
1162 score += 0.10;
1163 }
1164 if !entry.flows.is_empty() {
1165 score += 0.05;
1166 }
1167 if entry.exported && entry.component.is_some() {
1169 score += 0.05;
1170 }
1171
1172 let mut factor = 1.0;
1173 if is_generic_utility(&entry.qualified_name, &entry.path) {
1174 factor *= 0.5;
1175 }
1176 if path_has_test(&entry.path) {
1177 factor *= 0.5;
1178 }
1179 if is_generated(&entry.path) {
1180 factor *= 0.5;
1181 }
1182 if is_vendored(&entry.path) {
1183 factor *= 0.5;
1184 }
1185 if ubiquity(view, &entry.symbol_id) > UBIQUITY_THRESHOLD {
1186 factor *= 0.5;
1187 }
1188
1189 (score * factor).clamp(0.25, 1.25)
1190}
1191
1192pub const TASK_PPR_WEIGHT: f64 = 0.30;
1198pub const GLOBAL_PPR_WEIGHT: f64 = 0.20;
1200pub const LEXICAL_WEIGHT: f64 = 0.15;
1202pub const SEMANTIC_WEIGHT: f64 = 0.10;
1204pub const CONFIDENCE_WEIGHT: f64 = 0.10;
1206pub const CRITICALITY_WEIGHT: f64 = 0.10;
1208pub const CHANGE_RISK_WEIGHT: f64 = 0.05;
1210pub const NOVELTY_WEIGHT: f64 = 0.05;
1212pub const NO_TASK_GLOBAL_WEIGHT: f64 = 0.50;
1215
1216#[allow(clippy::too_many_arguments)]
1223pub fn final_importance(
1225 task_ppr: f64,
1226 global_ppr: f64,
1227 lexical: f64,
1228 semantic: f64,
1229 confidence: f64,
1230 criticality: f64,
1231 change_risk: f64,
1232 novelty: f64,
1233 has_task: bool,
1234) -> f64 {
1235 let (tw, gw) = if has_task {
1236 (TASK_PPR_WEIGHT, GLOBAL_PPR_WEIGHT)
1237 } else {
1238 (0.0, NO_TASK_GLOBAL_WEIGHT)
1239 };
1240 tw * task_ppr
1241 + gw * global_ppr
1242 + LEXICAL_WEIGHT * lexical
1243 + SEMANTIC_WEIGHT * semantic
1244 + CONFIDENCE_WEIGHT * confidence
1245 + CRITICALITY_WEIGHT * criticality
1246 + CHANGE_RISK_WEIGHT * change_risk
1247 + NOVELTY_WEIGHT * novelty
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use super::*;
1253 use scc_core::{entity_id, Entity, Relationship, Visibility};
1254 use scc_graph::{RealityGraph, TrustPolicy};
1255 use scc_store::Store;
1256 use std::collections::HashMap;
1257
1258fn fixture(
1260 entities: Vec<Entity>,
1261 rels: Vec<Relationship>,
1262 ) -> (tempfile::TempDir, Store, RealityGraph) {
1263 let dir = tempfile::TempDir::new().unwrap();
1264 let root = dir.path().join("repo");
1265 std::fs::create_dir_all(&root).unwrap();
1266 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
1267 for e in &entities {
1268 store.insert_entity(e, &["src/fixture.ts".to_string()]).unwrap();
1269 }
1270 for r in &rels {
1271 store.insert_relationship(r, "src/fixture.ts").unwrap();
1272 }
1273 let mut out: HashMap<String, Vec<Relationship>> = HashMap::new();
1274 let mut inn: HashMap<String, Vec<Relationship>> = HashMap::new();
1275 for r in &rels {
1276 out.entry(r.subject.clone()).or_default().push(r.clone());
1277 inn.entry(r.object.clone()).or_default().push(r.clone());
1278 }
1279 let graph = RealityGraph {
1280 repo_id: "r".into(),
1281 entities: entities.into_iter().map(|e| (e.id.clone(), e)).collect(),
1282 out,
1283 inn,
1284 components: vec![],
1285 flows: vec![],
1286 invariants: vec![],
1287 };
1288 (dir, store, graph)
1289 }
1290
1291fn sym(repo: &str, path: &str, name: &str) -> Entity {
1293 let mut e = Entity::new(
1294 scc_core::symbol_id(repo, path, name),
1295 kinds::SYMBOL,
1296 name,
1297 );
1298 e.attr("file", serde_json::json!(path));
1299 e.attr("exported", serde_json::json!(false));
1300 e.attr("start_line", serde_json::json!(1));
1301 e.attr("end_line", serde_json::json!(10));
1302 e
1303 }
1304
1305fn entity(id: &str, kind: &str, name: &str) -> Entity {
1307 Entity::new(id, kind, name)
1308 }
1309
1310fn rel(n: u64, subject: &str, pred: &str, object: &str, prov: Provenance) -> Relationship {
1312 Relationship::new(format!("rel:{n}"), subject, pred, object, prov)
1313 }
1314
1315 #[test]
1318fn reference_normalization_maps_kinds() {
1320 let (a, b, i, c, s, t, m, x, d) = (
1321 scc_core::symbol_id("r", "src/a.ts", "A"),
1322 scc_core::symbol_id("r", "src/b.ts", "B"),
1323 scc_core::symbol_id("r", "src/i.ts", "I"),
1324 "repo://r/contract/c",
1325 "repo://r/state/s",
1326 "repo://r/topic/t",
1327 "repo://r/module/m",
1328 "repo://r/export/x",
1329 "repo://r/annotation/d",
1330 );
1331 let mut a_ent = sym("r", "src/a.ts", "A");
1332 a_ent.attr("file", serde_json::json!("src/a.ts"));
1333 a_ent.attr("start_line", serde_json::json!(10));
1334 a_ent.attr("end_line", serde_json::json!(20));
1335 let b_ent = sym("r", "src/b.ts", "B");
1336 let i_ent = sym("r", "src/i.ts", "I");
1337 let c_ent = entity(c, kinds::CONTRACT, "c");
1338 let s_ent = entity(s, kinds::STATE, "s");
1339 let t_ent = entity(t, kinds::TOPIC, "t");
1340 let m_ent = entity(m, kinds::MODULE, "m");
1341 let x_ent = entity(x, kinds::EXPORT, "x");
1342 let d_ent = entity(d, kinds::ANNOTATION, "d");
1343
1344 let mut rels = vec![
1345 rel(1, &a, predicates::CALLS, &b, Provenance::Extracted),
1346 rel(2, &a, predicates::INVOKES, &b, Provenance::Extracted),
1347 rel(3, &a, predicates::HANDLES_CALLBACK, &b, Provenance::Extracted),
1348 rel(4, &a, predicates::IMPLEMENTS, &i, Provenance::Extracted),
1349 rel(5, &i, predicates::IMPLEMENTED_BY, &a, Provenance::Extracted),
1350 rel(6, &a, predicates::INHERITS, &b, Provenance::Extracted),
1351 rel(7, &a, predicates::REGISTERS, c, Provenance::Extracted),
1352 rel(8, &a, predicates::PUBLISHES, t, Provenance::Extracted),
1353 rel(9, &b, predicates::SUBSCRIBES, t, Provenance::Extracted),
1354 rel(10, &a, predicates::OWNS, s, Provenance::Extracted),
1355 rel(11, &a, predicates::READS, s, Provenance::Extracted),
1356 rel(12, &b, predicates::WRITES, s, Provenance::Extracted),
1357 rel(13, &a, predicates::IMPORTS, m, Provenance::Extracted),
1358 rel(14, &a, predicates::EXPORTS, x, Provenance::Extracted),
1359 rel(15, &a, predicates::DECORATES, d, Provenance::Extracted),
1360 rel(16, &a, predicates::OWNS, c, Provenance::Extracted),
1362 rel(17, &a, predicates::CALLS, d, Provenance::Extracted),
1364 ];
1365 rels[16].confidence = 0.7;
1366 let (_dir, store, graph) = fixture(
1367 vec![a_ent.clone(), b_ent, i_ent, c_ent, s_ent, t_ent, m_ent, x_ent, d_ent],
1368 rels,
1369 );
1370 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1371 let edges = build_reference_graph(&view);
1372
1373 let kinds_between = |src: &str, tgt: &str| -> Vec<ReferenceKind> {
1374 let mut v: Vec<ReferenceKind> = edges
1375 .iter()
1376 .filter(|e| e.source_symbol == src && e.target_symbol == tgt)
1377 .map(|e| e.kind)
1378 .collect();
1379 v.sort();
1380 v
1381 };
1382
1383 assert_eq!(
1386 kinds_between(&a, &b),
1387 vec![
1388 ReferenceKind::Call,
1389 ReferenceKind::Call,
1390 ReferenceKind::Call,
1391 ReferenceKind::Extend
1392 ]
1393 );
1394 assert_eq!(kinds_between(&a, &i), vec![ReferenceKind::Implement; 2]);
1397 assert_eq!(kinds_between(&a, c), vec![ReferenceKind::Register]);
1399 assert_eq!(kinds_between(&a, t), vec![ReferenceKind::Register]);
1400 assert_eq!(kinds_between(&b, t), vec![ReferenceKind::Register]);
1401 let mut as_edges = kinds_between(&a, s);
1403 as_edges.sort();
1404 assert_eq!(as_edges, vec![ReferenceKind::Read, ReferenceKind::Write]);
1405 assert_eq!(kinds_between(&b, s), vec![ReferenceKind::Write]);
1406 assert_eq!(kinds_between(&a, m), vec![ReferenceKind::Import]);
1408 assert_eq!(kinds_between(&a, x), vec![ReferenceKind::Export]);
1409 assert_eq!(kinds_between(&a, d), vec![ReferenceKind::Call, ReferenceKind::Decorate]);
1410 assert!(!kinds_between(&a, c).contains(&ReferenceKind::Write));
1412
1413 let call = edges
1415 .iter()
1416 .find(|e| e.source_symbol == a && e.target_symbol == b && e.kind == ReferenceKind::Call)
1417 .unwrap();
1418 assert!(call.locations.contains(&SourceRange::new("src/a.ts", 10, 20)));
1419 assert!(call.locations.contains(&SourceRange::new("src/b.ts", 1, 10)));
1420
1421 assert_eq!(call.confidence, 1.0);
1423 let low = edges
1424 .iter()
1425 .find(|e| e.source_symbol == a && e.target_symbol == d && e.kind == ReferenceKind::Call)
1426 .unwrap();
1427 assert!((low.confidence - 0.7).abs() < 1e-6);
1428 }
1429
1430 #[test]
1433fn rarity_downweights_ubiquitous() {
1435 let logger = rarity(100, 50);
1438 let distinctive = rarity(100, 1);
1439 assert!(logger < distinctive);
1440 assert!((logger - (100.0_f64 / 51.0).ln()).abs() < 1e-9);
1441
1442 assert_eq!(rarity(100, 10_000), 0.25);
1444 assert_eq!(rarity(100, 0), 1.5);
1445 assert_eq!(rarity(0, 0), 1.0);
1446
1447 let w_common = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 50);
1449 let w_rare = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 1);
1450 assert!(w_common < w_rare);
1451
1452 assert_eq!(edge_weight(predicates::CALLS, Provenance::Stale, 1.0, 100, 1), 0.0);
1455 let resolved = edge_weight(predicates::CALLS, Provenance::Resolved, 1.0, 100, 1);
1456 let extracted = edge_weight(predicates::CALLS, Provenance::Extracted, 1.0, 100, 1);
1457 assert!(resolved > extracted);
1458 }
1459
1460 #[test]
1463fn utility_pollution_ranks_below_public_api() {
1465 let api_id = scc_core::symbol_id("r", "src/api/order.ts", "OrderApi");
1466 let util_id = scc_core::symbol_id("r", "src/utils/helpers.ts", "helpers");
1467 let mut api_ent = sym("r", "src/api/order.ts", "OrderApi");
1468 api_ent.attr("exported", serde_json::json!(true));
1469 api_ent.attr("entrypoints", serde_json::json!(["http"]));
1470 let util_ent = sym("r", "src/utils/helpers.ts", "helpers");
1471
1472 let mut entities = vec![api_ent.clone(), util_ent.clone()];
1473 let mut rels: Vec<Relationship> = Vec::new();
1474 let mut n = 1u64;
1475 for i in 0..40 {
1477 let caller = scc_core::symbol_id("r", "src/callers/c.rs", &format!("c{i}"));
1478 entities.push(sym("r", "src/callers/c.rs", &format!("c{i}")));
1479 rels.push(rel(
1480 n,
1481 &caller,
1482 predicates::CALLS,
1483 &util_id,
1484 Provenance::Extracted,
1485 ));
1486 n += 1;
1487 }
1488 let c1 = scc_core::symbol_id("r", "src/callers/c.rs", "c0");
1490 let c2 = scc_core::symbol_id("r", "src/callers/c.rs", "c1");
1491 rels.push(rel(n, &c1, predicates::CALLS, &api_id, Provenance::Extracted));
1492 n += 1;
1493 rels.push(rel(n, &c2, predicates::CALLS, &api_id, Provenance::Extracted));
1494
1495 let (_dir, store, graph) = fixture(entities, rels);
1496 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1497 let ranker = SystemRanker::new(&view);
1498 let g = ranker.global_vector();
1499 let util_idx = ranker.index_of(&util_id).unwrap();
1500 let api_idx = ranker.index_of(&api_id).unwrap();
1501
1502 let util_entry = surface_entry(&util_id, "helpers", "src/utils/helpers.ts", false);
1505 let api_entry = surface_entry(&api_id, "OrderApi", "src/api/order.ts", true);
1506 let spec_util = architectural_specificity(&util_entry, &view);
1507 let spec_api = architectural_specificity(&api_entry, &view);
1508 assert_eq!(spec_util, 0.25);
1509 assert_eq!(spec_api, 1.25);
1510 assert!(spec_api > spec_util);
1511
1512 assert!(g[api_idx] * spec_api > g[util_idx] * spec_util);
1515 }
1516
1517 #[test]
1520fn task_personalization_ranks_billing_above_unrelated() {
1522 let billing_client = scc_core::symbol_id("r", "src/billing/client.ts", "BillingClient");
1523 let billing_retry = scc_core::symbol_id("r", "src/billing/client.ts", "BillingClient.retry");
1524 let billing_worker = scc_core::symbol_id("r", "src/billing/worker.ts", "BillingWorker");
1525 let billing_process = scc_core::symbol_id("r", "src/billing/worker.ts", "BillingWorker.process");
1526 let auth_service = scc_core::symbol_id("r", "src/auth/service.ts", "AuthService");
1527 let auth_login = scc_core::symbol_id("r", "src/auth/service.ts", "AuthService.login");
1528 let logger = scc_core::symbol_id("r", "src/logger.ts", "Logger");
1529 let logger_log = scc_core::symbol_id("r", "src/logger.ts", "Logger.log");
1530
1531 let entities = vec![
1532 sym("r", "src/billing/client.ts", "BillingClient"),
1533 sym("r", "src/billing/client.ts", "BillingClient.retry"),
1534 sym("r", "src/billing/worker.ts", "BillingWorker"),
1535 sym("r", "src/billing/worker.ts", "BillingWorker.process"),
1536 sym("r", "src/auth/service.ts", "AuthService"),
1537 sym("r", "src/auth/service.ts", "AuthService.login"),
1538 sym("r", "src/logger.ts", "Logger"),
1539 sym("r", "src/logger.ts", "Logger.log"),
1540 ];
1541 let rels = vec![
1542 rel(1, &billing_client, predicates::CALLS, &billing_retry, Provenance::Extracted),
1543 rel(2, &billing_worker, predicates::CALLS, &billing_process, Provenance::Extracted),
1544 rel(3, &billing_process, predicates::CALLS, &billing_retry, Provenance::Extracted),
1545 rel(4, &auth_service, predicates::CALLS, &auth_login, Provenance::Extracted),
1546 rel(5, &logger, predicates::CALLS, &logger_log, Provenance::Extracted),
1547 ];
1548
1549 let (_dir, store, graph) = fixture(entities, rels);
1550 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1551 let ranker = SystemRanker::new(&view);
1552
1553 let seeds = vec![
1554 TaskSeed {
1555 kind: "symbol".into(),
1556 id: billing_retry.clone(),
1557 weight: 1.0,
1558 },
1559 TaskSeed {
1560 kind: "symbol".into(),
1561 id: billing_process.clone(),
1562 weight: 1.0,
1563 },
1564 ];
1565 let tv = ranker.task_vector(&seeds);
1566 let i_retry = ranker.index_of(&billing_retry).unwrap();
1567 let i_process = ranker.index_of(&billing_process).unwrap();
1568 let i_login = ranker.index_of(&auth_login).unwrap();
1569 let i_log = ranker.index_of(&logger_log).unwrap();
1570
1571 assert!(tv[i_retry] > tv[i_login]);
1573 assert!(tv[i_retry] > tv[i_log]);
1574 assert!(tv[i_process] > tv[i_login]);
1575 assert!(tv[i_process] > tv[i_log]);
1576 assert!(tv[i_retry] > tv[ranker.index_of(&logger).unwrap()]);
1579
1580 let junk = vec![TaskSeed { kind: "symbol".into(), id: "repo://r/symbol/nope/Nope".into(), weight: 1.0 }];
1582 let fallback = ranker.task_vector(&junk);
1583 let g = ranker.global_vector();
1584 assert_eq!(fallback, g);
1585 assert_eq!(fallback.len(), tv.len());
1586 }
1587
1588 fn surface_entry(symbol_id: &str, name: &str, path: &str, exported: bool) -> SurfaceEntry {
1592 let mut entry = SurfaceEntry {
1593 id: symbol_id.to_string(),
1594 symbol_id: symbol_id.to_string(),
1595 qualified_name: name.to_string(),
1596 kind: scc_core::SurfaceKind::Function,
1597 path: path.to_string(),
1598 range: SourceRange::new(path, 1, 1),
1599 source_signature: String::new(),
1600 canonical_signature: String::new(),
1601 semantic_signature: scc_core::SemanticSignature::default(),
1602 visibility: Visibility::Public,
1603 exported,
1604 modifiers: vec![],
1605 annotations: vec![],
1606 component: Some("Order".to_string()),
1607 subsystem: None,
1608 flows: vec![],
1609 contracts: vec![],
1610 state_authorities: vec![],
1611 invocation_surfaces: vec![],
1612 callers: vec![],
1613 callees: vec![],
1614 caller_count: 0,
1615 callee_count: 0,
1616 importance: None,
1617 provenance: Provenance::Extracted,
1618 confidence: 1.0,
1619 rank: scc_core::SurfaceRank::default(),
1620 };
1621 if exported {
1622 entry.invocation_surfaces.push("http".into());
1623 entry.flows.push("f1".into());
1624 entry.contracts.push("c1".into());
1625 }
1626 entry
1627 }
1628
1629 #[test]
1632fn final_importance_blend_constants() {
1634 let v = final_importance(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, true);
1637 assert!((v - 1.05).abs() < 1e-9);
1638
1639 let tasky = final_importance(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true);
1641 assert!((tasky - 0.30).abs() < 1e-9);
1642
1643 let globaly = final_importance(1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false);
1645 assert!((globaly - 0.25).abs() < 1e-9);
1646
1647 let full = final_importance(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, false);
1650 assert!((full - 1.05).abs() < 1e-9);
1651 }
1652
1653 #[test]
1656fn specificity_penalizes_generic_test_and_vendored() {
1658 let (_dir, store, graph) = fixture(vec![sym("r", "src/x.ts", "X")], vec![]);
1659 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1660
1661 let util = surface_entry("s1", "logger", "src/utils/logger.ts", false);
1662 assert_eq!(architectural_specificity(&util, &view), 0.5);
1665
1666 let testy = surface_entry("s2", "TestHelper", "tests/unit/helper_test.rs", false);
1667 assert_eq!(architectural_specificity(&testy, &view), 0.5);
1668
1669 let vendored = surface_entry("s3", "Dep", "vendor/dep/src/lib.rs", false);
1670 assert_eq!(architectural_specificity(&vendored, &view), 0.5);
1671
1672 let generated = surface_entry("s4", "Proto", "src/generated/models.rs", false);
1673 assert_eq!(architectural_specificity(&generated, &view), 0.5);
1674
1675 let facade = surface_entry("s5", "OrderApi", "src/api/order.ts", true);
1677 assert_eq!(architectural_specificity(&facade, &view), 1.25);
1678 }
1679
1680 #[test]
1683fn ranker_universe_is_heterogeneous() {
1685 let owner = scc_core::symbol_id("r", "src/a.ts", "A");
1686 let state_id = entity_id("r", kinds::STATE, "sessions");
1687 let contract_id = entity_id("r", kinds::CONTRACT, "c1");
1688 let topic_id = entity_id("r", kinds::TOPIC, "orders");
1689 let store_id = entity_id("r", kinds::DATA_STORE, "pg");
1690 let flow_id = entity_id("r", kinds::FLOW, "signup");
1691 let file_id = entity_id("r", kinds::FILE, "src/a.ts");
1692
1693 let mut owner_ent = sym("r", "src/a.ts", "A");
1694 owner_ent.attr("exported", serde_json::json!(true));
1695 owner_ent.attr("entrypoints", serde_json::json!(["http"]));
1696 let entities = vec![
1697 owner_ent,
1698 entity(&state_id, kinds::STATE, "sessions"),
1699 entity(&contract_id, kinds::CONTRACT, "c1"),
1700 entity(&topic_id, kinds::TOPIC, "orders"),
1701 entity(&store_id, kinds::DATA_STORE, "pg"),
1702 entity(&flow_id, kinds::FLOW, "signup"),
1703 entity(&file_id, kinds::FILE, "src/a.ts"),
1704 ];
1705 let n = 1u64;
1706 let rels = vec![
1707 rel(n, &owner, predicates::OWNS, &state_id, Provenance::Extracted),
1708 rel(n + 1, &owner, predicates::REGISTERS, &contract_id, Provenance::Extracted),
1709 rel(n + 2, &owner, predicates::PUBLISHES, &topic_id, Provenance::Extracted),
1710 rel(n + 3, &owner, predicates::READS, &store_id, Provenance::Extracted),
1711 rel(n + 4, &owner, predicates::WRITES, &store_id, Provenance::Extracted),
1712 rel(n + 5, &owner, predicates::PARTICIPATES_IN, &flow_id, Provenance::Extracted),
1713 ];
1714
1715 let (_dir, store, graph) = fixture(entities, rels);
1716 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1717 let ranker = SystemRanker::new(&view);
1718
1719 for id in [
1721 owner.as_str(),
1722 state_id.as_str(),
1723 contract_id.as_str(),
1724 topic_id.as_str(),
1725 store_id.as_str(),
1726 flow_id.as_str(),
1727 file_id.as_str(),
1728 ] {
1729 assert!(ranker.index_of(id).is_some(), "{id} must be a node");
1730 }
1731 assert!(ranker.index_of(&entity_id("r", kinds::MODULE, "m")).is_none());
1733
1734 let g = ranker.global_vector();
1736 assert_eq!(g.len(), ranker.nodes().len());
1737
1738 let owner_i = ranker.index_of(&owner).unwrap();
1740 let state_i = ranker.index_of(&state_id).unwrap();
1741 assert!(g[state_i] > 0.0, "state node must receive PPR mass");
1742 assert!(g[owner_i] > 0.0);
1743
1744 let syms = ranker.symbols();
1746 assert_eq!(syms, vec![owner.clone()]);
1747 }
1748
1749 #[test]
1750fn project_to_symbols_lifts_owned_entity_scores() {
1752 let owner = scc_core::symbol_id("r", "src/a.ts", "A");
1753 let other = scc_core::symbol_id("r", "src/b.ts", "B");
1754 let state_id = entity_id("r", kinds::STATE, "sessions");
1755 let contract_id = entity_id("r", kinds::CONTRACT, "c1");
1756
1757 let mut owner_ent = sym("r", "src/a.ts", "A");
1758 owner_ent.attr("exported", serde_json::json!(true));
1759 owner_ent.attr("entrypoints", serde_json::json!(["http"]));
1760 let entities = vec![
1761 owner_ent,
1762 sym("r", "src/b.ts", "B"),
1763 entity(&state_id, kinds::STATE, "sessions"),
1764 entity(&contract_id, kinds::CONTRACT, "c1"),
1765 ];
1766 let rels = vec![
1767 rel(1, &owner, predicates::OWNS, &state_id, Provenance::Extracted),
1768 rel(2, &owner, predicates::REGISTERS, &contract_id, Provenance::Extracted),
1769 rel(3, &other, predicates::OWNS, &state_id, Provenance::Extracted),
1771 ];
1772 let (_dir, store, graph) = fixture(entities, rels);
1773 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1774 let ranker = SystemRanker::new(&view);
1775
1776 let g = ranker.global_vector();
1777 let projected = ranker.project_to_symbols(&g);
1778 let score_of = |id: &str| -> f64 {
1779 projected
1780 .iter()
1781 .find(|(s, _)| s == id)
1782 .map(|(_, v)| *v)
1783 .unwrap_or(f64::NAN)
1784 };
1785
1786 let own_a = score_of(&owner);
1787 let own_b = score_of(&other);
1788 let state_score = g[ranker.index_of(&state_id).unwrap()];
1789 let contract_score = g[ranker.index_of(&contract_id).unwrap()];
1790
1791 let bonus_a = (0.4 * (state_score + contract_score)).min(0.5);
1794 let bonus_b = (0.4 * state_score).min(0.5);
1795 assert!((own_a - (g[ranker.index_of(&owner).unwrap()] + bonus_a)).abs() < 1e-9);
1796 assert!((own_b - (g[ranker.index_of(&other).unwrap()] + bonus_b)).abs() < 1e-9);
1797 assert!(own_a > own_b);
1799
1800 let mut big_v = vec![0.0; g.len()];
1802 big_v[ranker.index_of(&state_id).unwrap()] = 100.0;
1803 big_v[ranker.index_of(&contract_id).unwrap()] = 100.0;
1804 let capped = ranker.project_to_symbols(&big_v);
1805 let cap_a = capped.iter().find(|(s, _)| s == &owner).unwrap().1;
1806 let own_a0 = big_v[ranker.index_of(&owner).unwrap()];
1807 assert!((cap_a - (own_a0 + 0.5)).abs() < 1e-9);
1808
1809 let again = ranker.project_to_symbols(&g);
1811 assert_eq!(again, projected);
1812 }
1813
1814 #[test]
1817fn rank_edge_kind_direction_and_weights() {
1819 for (container, member) in [
1830 (kinds::SYSTEM, kinds::SUBSYSTEM),
1831 (kinds::SUBSYSTEM, kinds::SERVICE),
1832 (kinds::SUBSYSTEM, kinds::COMPONENT),
1833 (kinds::SERVICE, kinds::COMPONENT),
1834 (kinds::COMPONENT, kinds::FILE),
1835 (kinds::COMPONENT, kinds::SYMBOL),
1836 (kinds::FILE, kinds::SYMBOL),
1837 (kinds::SYSTEM, kinds::SERVICE),
1840 (kinds::SYSTEM, kinds::FILE),
1841 (kinds::SUBSYSTEM, kinds::SYMBOL),
1842 (kinds::SERVICE, kinds::FILE),
1843 ] {
1844 assert_eq!(
1845 rank_edge_kind(predicates::CONTAINS, container, member),
1846 Some(RankEdgeKind::Contains)
1847 );
1848 assert_eq!(
1849 rank_edge_kind(predicates::CONTAINS, member, container),
1850 Some(RankEdgeKind::MemberOf)
1851 );
1852 }
1853 assert_eq!(
1855 rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::COMPONENT),
1856 Some(RankEdgeKind::MemberOf)
1857 );
1858 assert_eq!(
1859 rank_edge_kind(predicates::PARTICIPATES_IN, kinds::SYMBOL, kinds::FLOW),
1860 Some(RankEdgeKind::ParticipatesIn)
1861 );
1862 assert_eq!(
1863 rank_edge_kind(predicates::PARTICIPATES_IN, kinds::FLOW, kinds::SYMBOL),
1864 Some(RankEdgeKind::FlowReachesParticipant)
1865 );
1866 assert_eq!(
1867 rank_edge_kind(predicates::HANDLES, kinds::SYMBOL, kinds::ROUTE),
1868 Some(RankEdgeKind::Handles)
1869 );
1870 assert_eq!(
1871 rank_edge_kind(predicates::DEFINES, kinds::SYMBOL, kinds::SCHEMA),
1872 Some(RankEdgeKind::Defines)
1873 );
1874 assert_eq!(
1877 rank_edge_kind(predicates::HANDLES, kinds::ROUTE, kinds::SYMBOL),
1878 Some(RankEdgeKind::HandledBy)
1879 );
1880 assert_eq!(
1881 rank_edge_kind(predicates::DEFINES, kinds::SCHEMA, kinds::SYMBOL),
1882 Some(RankEdgeKind::DefinedBy)
1883 );
1884
1885 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::COMPONENT, kinds::FLOW), None);
1887 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::CONTRACT), None);
1888 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::FLOW), None);
1889 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::COMPONENT, kinds::COMPONENT), None);
1891 assert_eq!(rank_edge_kind(predicates::CONTAINS, kinds::SYMBOL, kinds::SYMBOL), None);
1892 assert_eq!(rank_edge_kind(predicates::PARTICIPATES_IN, kinds::SYMBOL, kinds::CONTRACT), None);
1893 assert_eq!(rank_edge_kind(predicates::HANDLES, kinds::SYMBOL, kinds::ENDPOINT), None);
1894 assert_eq!(rank_edge_kind(predicates::DEFINES, kinds::SYMBOL, kinds::CONTRACT), None);
1895 assert_eq!(rank_edge_kind(predicates::CALLS, kinds::SYMBOL, kinds::SYMBOL), None);
1896
1897 assert_eq!(RankEdgeKind::Contains.weight(), RANK_MEMBERSHIP);
1899 assert_eq!(RankEdgeKind::MemberOf.weight(), RANK_MEMBER_OF);
1900 assert_eq!(RankEdgeKind::ParticipatesIn.weight(), RANK_PARTICIPATES);
1901 assert_eq!(RankEdgeKind::FlowReachesParticipant.weight(), RANK_FLOW_REACHES_PARTICIPANT);
1902 assert_eq!(RankEdgeKind::Handles.weight(), RANK_HANDLES);
1903 assert_eq!(RankEdgeKind::HandledBy.weight(), RANK_HANDLED_BY);
1904 assert_eq!(RankEdgeKind::Defines.weight(), RANK_DEFINES);
1905 assert_eq!(RankEdgeKind::DefinedBy.weight(), RANK_DEFINED_BY);
1906
1907 assert_eq!(RankEdgeKind::Contains.reverse_ranking_transition(), Some(RankEdgeKind::MemberOf));
1913 assert_eq!(RankEdgeKind::MemberOf.reverse_ranking_transition(), Some(RankEdgeKind::Contains));
1914 assert_eq!(RankEdgeKind::ParticipatesIn.reverse_ranking_transition(), Some(RankEdgeKind::FlowReachesParticipant));
1915 assert_eq!(RankEdgeKind::FlowReachesParticipant.reverse_ranking_transition(), Some(RankEdgeKind::ParticipatesIn));
1916 assert_eq!(RankEdgeKind::Handles.reverse_ranking_transition(), Some(RankEdgeKind::HandledBy));
1917 assert_eq!(RankEdgeKind::HandledBy.reverse_ranking_transition(), Some(RankEdgeKind::Handles));
1918 assert_eq!(RankEdgeKind::Defines.reverse_ranking_transition(), Some(RankEdgeKind::DefinedBy));
1919 assert_eq!(RankEdgeKind::DefinedBy.reverse_ranking_transition(), Some(RankEdgeKind::Defines));
1920 }
1921
1922 #[test]
1923fn contains_edges_spread_component_importance_both_ways() {
1925 let comp_id = entity_id("r", kinds::COMPONENT, "orders");
1931 let m1 = scc_core::symbol_id("r", "src/orders/a.ts", "OrderService");
1932 let m2 = scc_core::symbol_id("r", "src/orders/b.ts", "OrderRepo");
1933 let x = scc_core::symbol_id("r", "src/other.ts", "Unrelated");
1934
1935 let entities = vec![
1936 entity(&comp_id, kinds::COMPONENT, "orders"),
1937 sym("r", "src/orders/a.ts", "OrderService"),
1938 sym("r", "src/orders/b.ts", "OrderRepo"),
1939 sym("r", "src/other.ts", "Unrelated"),
1940 ];
1941 let rels = vec![
1942 rel(1, &comp_id, predicates::CONTAINS, &m1, Provenance::Extracted),
1943 rel(2, &comp_id, predicates::CONTAINS, &m2, 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 c = g[ranker.index_of(&comp_id).unwrap()];
1951 let s1 = g[ranker.index_of(&m1).unwrap()];
1952 let s2 = g[ranker.index_of(&m2).unwrap()];
1953 let sx = g[ranker.index_of(&x).unwrap()];
1954
1955 assert!(c > 0.3, "component importance derived from members: {c}");
1961 assert!(s1 > sx && s2 > sx, "members lifted above the isolated symbol");
1962 assert!(c > s1 && c > s2, "component spreads importance down to members");
1963 }
1964
1965 #[test]
1966fn participates_in_connects_flow_nodes() {
1968 let s = scc_core::symbol_id("r", "src/a.ts", "A");
1974 let f1 = entity_id("r", kinds::FLOW, "signup");
1975 let f2 = entity_id("r", kinds::FLOW, "checkout");
1976
1977 let entities = vec![
1978 sym("r", "src/a.ts", "A"),
1979 entity(&f1, kinds::FLOW, "signup"),
1980 entity(&f2, kinds::FLOW, "checkout"),
1981 ];
1982 let rels = vec![rel(1, &s, predicates::PARTICIPATES_IN, &f1, Provenance::Extracted)];
1983 let (_dir, store, graph) = fixture(entities, rels);
1984 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
1985 let ranker = SystemRanker::new(&view);
1986 let g = ranker.global_vector();
1987
1988 let f1v = g[ranker.index_of(&f1).unwrap()];
1989 let f2v = g[ranker.index_of(&f2).unwrap()];
1990 let sv = g[ranker.index_of(&s).unwrap()];
1991
1992 assert!(f1v > 0.3, "flow with a participant receives PPR mass: {f1v}");
1996 assert!(f1v > f2v, "connected flow ranks above the bare flow");
1997 assert!(sv > f2v, "flow importance reaches the participant: {sv} vs {f2v}");
1999 }
2000
2001 #[test]
2002fn handles_and_defines_edges_feed_ppr() {
2004 let h = scc_core::symbol_id("r", "src/api.ts", "OrderHandler");
2013 let route = entity_id("r", kinds::ROUTE, "/orders");
2014 let d = scc_core::symbol_id("r", "src/models.ts", "Order");
2015 let schema = entity_id("r", kinds::SCHEMA, "order");
2016 let x = scc_core::symbol_id("r", "src/util.ts", "Helper");
2017
2018 let mut d_ent = sym("r", "src/models.ts", "Order");
2019 d_ent.attr("exported", serde_json::json!(true));
2020 let entities = vec![
2021 sym("r", "src/api.ts", "OrderHandler"),
2022 entity(&route, kinds::ROUTE, "/orders"),
2023 d_ent,
2024 entity(&schema, kinds::SCHEMA, "order"),
2025 sym("r", "src/util.ts", "Helper"),
2026 ];
2027 let rels = vec![
2028 rel(1, &h, predicates::HANDLES, &route, Provenance::Extracted),
2029 rel(2, &d, predicates::DEFINES, &schema, Provenance::Extracted),
2030 ];
2031 let (_dir, store, graph) = fixture(entities, rels);
2032 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2033 let ranker = SystemRanker::new(&view);
2034 let g = ranker.global_vector();
2035
2036 let rv = g[ranker.index_of(&route).unwrap()];
2037 let sv = g[ranker.index_of(&schema).unwrap()];
2038 let xv = g[ranker.index_of(&x).unwrap()];
2039
2040 assert!(rv > xv, "route receives mass via the HANDLES edge: {rv} vs {xv}");
2041 assert!(sv > xv, "schema receives mass via the DEFINES edge: {sv} vs {xv}");
2042 }
2043
2044 #[test]
2047fn file_contains_symbol_lifts_member() {
2049 let file_id = entity_id("r", kinds::FILE, "src/orders.ts");
2057 let sym_id = scc_core::symbol_id("r", "src/orders.ts", "OrderService");
2058 let x = scc_core::symbol_id("r", "src/other.ts", "Unrelated");
2059
2060 let entities = vec![
2061 entity(&file_id, kinds::FILE, "src/orders.ts"),
2062 sym("r", "src/orders.ts", "OrderService"),
2063 sym("r", "src/other.ts", "Unrelated"),
2064 ];
2065 let rels = vec![rel(1, &file_id, predicates::CONTAINS, &sym_id, Provenance::Extracted)];
2066 let (_dir, store, graph) = fixture(entities, rels);
2067 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2068 let ranker = SystemRanker::new(&view);
2069 let g = ranker.global_vector();
2070
2071 let fv = g[ranker.index_of(&file_id).unwrap()];
2072 let sv = g[ranker.index_of(&sym_id).unwrap()];
2073 let xv = g[ranker.index_of(&x).unwrap()];
2074
2075 assert!(fv > 0.3, "file node derives importance from its symbol: {fv}");
2079 assert!(sv > xv, "contained symbol lifted above the isolated one: {sv} vs {xv}");
2080 assert!(fv > xv, "file lifted above the isolated symbol: {fv} vs {xv}");
2081 }
2082
2083 #[test]
2084fn subsystem_component_containment_spreads_both_ways() {
2086 let sub_id = entity_id("r", kinds::SUBSYSTEM, "billing");
2092 let comp_id = entity_id("r", kinds::COMPONENT, "orders");
2093 let bare = entity_id("r", kinds::COMPONENT, "auth");
2094
2095 let entities = vec![
2096 entity(&sub_id, kinds::SUBSYSTEM, "billing"),
2097 entity(&comp_id, kinds::COMPONENT, "orders"),
2098 entity(&bare, kinds::COMPONENT, "auth"),
2099 ];
2100 let rels = vec![rel(1, &sub_id, predicates::CONTAINS, &comp_id, Provenance::Extracted)];
2101 let (_dir, store, graph) = fixture(entities, rels);
2102 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2103 let ranker = SystemRanker::new(&view);
2104 let g = ranker.global_vector();
2105
2106 let subv = g[ranker.index_of(&sub_id).unwrap()];
2107 let compv = g[ranker.index_of(&comp_id).unwrap()];
2108 let barev = g[ranker.index_of(&bare).unwrap()];
2109
2110 assert!(compv > barev, "contained component ranks above the bare one: {compv} vs {barev}");
2114 assert!(subv > barev, "subsystem derives importance from its component: {subv} vs {barev}");
2115 assert!(subv > 0.3, "subsystem is a first-class rank node: {subv}");
2116 }
2117
2118 #[test]
2121fn hot_route_projects_to_handler_symbol() {
2123 let handler = scc_core::symbol_id("r", "src/router.ts", "build_router");
2130 let route = entity_id("r", kinds::ROUTE, "/health");
2131 let other = scc_core::symbol_id("r", "src/util.ts", "Helper");
2132
2133 let entities = vec![
2134 sym("r", "src/router.ts", "build_router"),
2135 entity(&route, kinds::ROUTE, "/health"),
2136 sym("r", "src/util.ts", "Helper"),
2137 ];
2138 let rels = vec![rel(1, &handler, predicates::HANDLES, &route, Provenance::Extracted)];
2139 let (_dir, store, graph) = fixture(entities, rels);
2140 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2141 let ranker = SystemRanker::new(&view);
2142
2143 let seeds = vec![TaskSeed {
2145 kind: "route".into(),
2146 id: route.clone(),
2147 weight: 1.0,
2148 }];
2149 let tv = ranker.task_vector(&seeds);
2150 let projected = ranker.project_to_symbols(&tv);
2151 let score_of = |id: &str| -> f64 {
2152 projected
2153 .iter()
2154 .find(|(s, _)| s == id)
2155 .map(|(_, v)| *v)
2156 .unwrap_or(f64::NAN)
2157 };
2158
2159 let route_mass = tv[ranker.index_of(&route).unwrap()];
2160 let handler_own = tv[ranker.index_of(&handler).unwrap()];
2161 let handler_score = score_of(&handler);
2162 let other_score = score_of(&other);
2163
2164 let expected_bonus = (PROJECTION_BONUS_FACTOR * route_mass).min(PROJECTION_BONUS_CAP);
2168 assert!((handler_score - (handler_own + expected_bonus)).abs() < 1e-9);
2169 assert!(route_mass > 0.1, "seeded route is hot: {route_mass}");
2170 assert!(handler_score > other_score, "hot route lifts its handler above unrelated symbols");
2171 }
2172
2173 #[test]
2174fn hot_schema_projects_to_definer_symbol() {
2176 let definer = scc_core::symbol_id("r", "src/models.ts", "Order");
2181 let schema = entity_id("r", kinds::SCHEMA, "order");
2182 let other = scc_core::symbol_id("r", "src/util.ts", "Helper");
2183
2184 let entities = vec![
2185 sym("r", "src/models.ts", "Order"),
2186 entity(&schema, kinds::SCHEMA, "order"),
2187 sym("r", "src/util.ts", "Helper"),
2188 ];
2189 let rels = vec![rel(1, &definer, predicates::DEFINES, &schema, Provenance::Extracted)];
2190 let (_dir, store, graph) = fixture(entities, rels);
2191 let view = TrustedGraphView::new(&graph, &store, &[], TrustPolicy::default());
2192 let ranker = SystemRanker::new(&view);
2193
2194 let seeds = vec![TaskSeed {
2195 kind: "schema".into(),
2196 id: schema.clone(),
2197 weight: 1.0,
2198 }];
2199 let tv = ranker.task_vector(&seeds);
2200 let projected = ranker.project_to_symbols(&tv);
2201 let score_of = |id: &str| -> f64 {
2202 projected
2203 .iter()
2204 .find(|(s, _)| s == id)
2205 .map(|(_, v)| *v)
2206 .unwrap_or(f64::NAN)
2207 };
2208
2209 let schema_mass = tv[ranker.index_of(&schema).unwrap()];
2210 let definer_own = tv[ranker.index_of(&definer).unwrap()];
2211 let definer_score = score_of(&definer);
2212 let other_score = score_of(&other);
2213
2214 let expected_bonus = (PROJECTION_BONUS_FACTOR * schema_mass).min(PROJECTION_BONUS_CAP);
2215 assert!((definer_score - (definer_own + expected_bonus)).abs() < 1e-9);
2216 assert!(schema_mass > 0.1, "seeded schema is hot: {schema_mass}");
2217 assert!(definer_score > other_score, "hot schema lifts its definer above unrelated symbols");
2218 }
2219}