tsift_resolution/
resolve.rs1use std::collections::{BTreeMap, BTreeSet, HashMap};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
4pub struct StrategyRank {
5 pub priority: usize,
6 pub tie_breaker: usize,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct RankedMatch<T> {
11 pub item: T,
12 pub score: usize,
13}
14
15pub fn token_overlap_rank<'a, T>(
16 query_tokens: &BTreeSet<String>,
17 entries: &'a [T],
18 index: &HashMap<String, Vec<usize>>,
19) -> Vec<(usize, &'a T)> {
20 let mut scores = BTreeMap::<usize, usize>::new();
21 for token in query_tokens {
22 if let Some(indices) = index.get(token) {
23 for idx in indices {
24 *scores.entry(*idx).or_default() += 1;
25 }
26 }
27 }
28 let mut matches = scores
29 .into_iter()
30 .map(|(idx, score)| (score, &entries[idx]))
31 .collect::<Vec<_>>();
32 matches.sort_by(|(left_score, _), (right_score, _)| right_score.cmp(left_score));
33 matches
34}
35
36pub fn f1_score(precision: f64, recall: f64) -> f64 {
37 if precision + recall <= 0.0 {
38 return 0.0;
39 }
40 2.0 * precision * recall / (precision + recall)
41}
42
43pub fn tag_f1_score(matching_tags: usize, query_tag_count: usize, symbol_tag_count: usize) -> f64 {
44 if query_tag_count == 0 || symbol_tag_count == 0 {
45 return 0.0;
46 }
47 let precision = matching_tags as f64 / symbol_tag_count as f64;
48 let recall = matching_tags as f64 / query_tag_count as f64;
49 f1_score(precision, recall)
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum NodeMatchKind {
54 ExactHandle,
55 PathComponent,
56 RefId,
57 Label,
58}
59
60impl NodeMatchKind {
61 pub fn priority(self) -> usize {
62 match self {
63 NodeMatchKind::ExactHandle => 0,
64 NodeMatchKind::PathComponent => 1,
65 NodeMatchKind::RefId => 2,
66 NodeMatchKind::Label => 3,
67 }
68 }
69}
70
71pub fn kind_priority(kind: &str) -> usize {
72 match kind {
73 "file" => 1,
74 "symbol" => 2,
75 "session" => 3,
76 "backlog" => 4,
77 "job_packet" => 5,
78 "worker_result" => 6,
79 "source_handle" => 7,
80 "worker_context" => 8,
81 _ => 99,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn f1_score_perfect() {
91 let score = f1_score(1.0, 1.0);
92 assert!((score - 1.0).abs() < f64::EPSILON);
93 }
94
95 #[test]
96 fn f1_score_zero_denominator() {
97 let score = f1_score(0.0, 0.0);
98 assert!(score.abs() < f64::EPSILON);
99 }
100
101 #[test]
102 fn f1_score_balanced() {
103 let score = f1_score(0.5, 0.5);
104 assert!((score - 0.5).abs() < f64::EPSILON);
105 }
106
107 #[test]
108 fn tag_f1_score_basic() {
109 let score = tag_f1_score(3, 5, 4);
110 let expected = f1_score(3.0 / 4.0, 3.0 / 5.0);
111 assert!((score - expected).abs() < f64::EPSILON);
112 }
113
114 #[test]
115 fn tag_f1_score_zero_query() {
116 assert_eq!(tag_f1_score(0, 0, 5), 0.0);
117 }
118
119 #[test]
120 fn tag_f1_score_zero_symbol() {
121 assert_eq!(tag_f1_score(0, 5, 0), 0.0);
122 }
123
124 #[test]
125 fn token_overlap_rank_basic() {
126 let entries = vec!["alpha", "beta", "gamma"];
127 let mut index = HashMap::new();
128 index.insert("tok1".to_string(), vec![0, 1]);
129 index.insert("tok2".to_string(), vec![1, 2]);
130 let tokens: BTreeSet<String> = ["tok1".to_string(), "tok2".to_string()]
131 .into_iter()
132 .collect();
133 let ranked = token_overlap_rank(&tokens, &entries, &index);
134 assert_eq!(ranked[0].0, 2);
135 assert_eq!(*ranked[0].1, "beta");
136 assert_eq!(ranked[1].0, 1);
137 }
138
139 #[test]
140 fn token_overlap_rank_no_matches() {
141 let entries = vec!["alpha"];
142 let index = HashMap::new();
143 let tokens: BTreeSet<String> = ["missing".to_string()].into_iter().collect();
144 let ranked = token_overlap_rank(&tokens, &entries, &index);
145 assert!(ranked.is_empty());
146 }
147
148 #[test]
149 fn node_match_kind_priority_order() {
150 assert!(NodeMatchKind::ExactHandle.priority() < NodeMatchKind::PathComponent.priority());
151 assert!(NodeMatchKind::PathComponent.priority() < NodeMatchKind::RefId.priority());
152 assert!(NodeMatchKind::RefId.priority() < NodeMatchKind::Label.priority());
153 }
154
155 #[test]
156 fn kind_priority_file_is_highest() {
157 assert_eq!(kind_priority("file"), 1);
158 assert!(kind_priority("file") < kind_priority("symbol"));
159 assert!(kind_priority("symbol") < kind_priority("session"));
160 assert!(kind_priority("unknown") > kind_priority("worker_context"));
161 }
162}