Skip to main content

tsift_resolution/
resolve.rs

1use 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}