Skip to main content

webspec_index/analyze/
coverage.rs

1//! Coverage computation for spec algorithm step tracking.
2
3use super::matcher::MatchResult;
4use super::scanner::StepComment;
5use super::steps::{flatten_steps, AlgorithmStep};
6
7/// Coverage of a spec algorithm in source code.
8#[derive(Debug, Clone)]
9pub struct CoverageResult {
10    pub anchor: String,
11    pub total_steps: usize,
12    pub implemented: Vec<Vec<u32>>,
13    pub missing: Vec<Vec<u32>>,
14    pub warnings: usize,
15    pub reordered: usize,
16}
17
18impl CoverageResult {
19    pub fn implemented_count(&self) -> usize {
20        self.implemented.len()
21    }
22
23    /// One-line summary for code lens display.
24    pub fn summary(&self) -> String {
25        let mut parts = vec![format!(
26            "{}: {}/{} steps",
27            self.anchor,
28            self.implemented_count(),
29            self.total_steps
30        )];
31        if self.warnings > 0 {
32            let s = if self.warnings != 1 { "s" } else { "" };
33            parts.push(format!("{} warning{s}", self.warnings));
34        }
35        if self.reordered > 0 {
36            parts.push(format!("{} reordered", self.reordered));
37        }
38        parts.join(" | ")
39    }
40}
41
42/// Length of the longest strictly increasing subsequence (O(n log n) patience sort).
43fn longest_increasing_subsequence_length(seq: &[usize]) -> usize {
44    if seq.is_empty() {
45        return 0;
46    }
47    let mut tails: Vec<usize> = Vec::new();
48    for &val in seq {
49        match tails.binary_search(&val) {
50            Ok(_) => {} // duplicate, don't extend
51            Err(pos) => {
52                if pos == tails.len() {
53                    tails.push(val);
54                } else {
55                    tails[pos] = val;
56                }
57            }
58        }
59    }
60    tails.len()
61}
62
63/// A step validation result pairing a source comment with its match outcome.
64#[derive(Debug, Clone)]
65pub struct StepValidation {
66    pub step: StepComment,
67    pub result: MatchResult,
68    pub spec_text: String,
69    pub algo_anchor: String,
70}
71
72/// Compute coverage of an algorithm from step validations.
73pub fn compute_coverage(
74    validations: &[StepValidation],
75    algo_steps: &[AlgorithmStep],
76    anchor: &str,
77) -> CoverageResult {
78    let flat = flatten_steps(algo_steps);
79    let total = flat.len();
80
81    // Build lookup: step number tuple -> flat index
82    let mut step_to_idx = std::collections::HashMap::new();
83    let mut all_numbers = std::collections::HashSet::new();
84    for (i, s) in flat.iter().enumerate() {
85        step_to_idx.insert(s.number.clone(), i);
86        all_numbers.insert(s.number.clone());
87    }
88
89    let mut implemented: Vec<Vec<u32>> = Vec::new();
90    let mut implemented_set = std::collections::HashSet::new();
91    let mut spec_order_indices: Vec<usize> = Vec::new();
92    let mut warnings = 0;
93
94    for v in validations {
95        let key = v.step.number.clone();
96        match v.result {
97            MatchResult::Exact | MatchResult::Fuzzy => {
98                if !implemented_set.contains(&key) {
99                    implemented.push(key.clone());
100                    implemented_set.insert(key.clone());
101                    if let Some(&idx) = step_to_idx.get(&key) {
102                        spec_order_indices.push(idx);
103                    }
104                }
105            }
106            MatchResult::Mismatch => {
107                if !implemented_set.contains(&key) {
108                    implemented.push(key.clone());
109                    implemented_set.insert(key.clone());
110                    if let Some(&idx) = step_to_idx.get(&key) {
111                        spec_order_indices.push(idx);
112                    }
113                }
114                warnings += 1;
115            }
116            MatchResult::NotFound => {
117                warnings += 1;
118            }
119        }
120    }
121
122    let missing: Vec<Vec<u32>> = flat
123        .iter()
124        .filter(|s| !implemented_set.contains(&s.number))
125        .map(|s| s.number.clone())
126        .collect();
127
128    let lis_len = longest_increasing_subsequence_length(&spec_order_indices);
129    let reordered = spec_order_indices.len().saturating_sub(lis_len);
130
131    CoverageResult {
132        anchor: anchor.to_string(),
133        total_steps: total,
134        implemented,
135        missing,
136        warnings,
137        reordered,
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::analyze::steps::parse_steps;
145
146    const SIMPLE_ALGO: &str = "1. First.\n2. Second.\n3. Third.";
147    const NESTED_ALGO: &str = "1. Parent.\n\n    1. Child one.\n    2. Child two.\n2. Other.\n";
148
149    fn fake_validation(number: Vec<u32>, result: MatchResult) -> StepValidation {
150        StepValidation {
151            step: StepComment {
152                line: 0,
153                col_start: 0,
154                col_end: 10,
155                indent: 0,
156                number,
157                text: String::new(),
158                end_line: None,
159            },
160            result,
161            spec_text: String::new(),
162            algo_anchor: String::new(),
163        }
164    }
165
166    // ── LIS tests ──
167
168    #[test]
169    fn lis_empty() {
170        assert_eq!(longest_increasing_subsequence_length(&[]), 0);
171    }
172
173    #[test]
174    fn lis_single() {
175        assert_eq!(longest_increasing_subsequence_length(&[5]), 1);
176    }
177
178    #[test]
179    fn lis_sorted() {
180        assert_eq!(longest_increasing_subsequence_length(&[1, 2, 3, 4, 5]), 5);
181    }
182
183    #[test]
184    fn lis_reverse() {
185        assert_eq!(longest_increasing_subsequence_length(&[5, 4, 3, 2, 1]), 1);
186    }
187
188    #[test]
189    fn lis_mixed() {
190        assert_eq!(longest_increasing_subsequence_length(&[1, 3, 2, 5]), 3);
191    }
192
193    #[test]
194    fn lis_duplicates() {
195        assert_eq!(longest_increasing_subsequence_length(&[1, 1, 1]), 1);
196    }
197
198    #[test]
199    fn lis_longer_sequence() {
200        assert_eq!(
201            longest_increasing_subsequence_length(&[3, 1, 4, 1, 5, 9, 2, 6]),
202            4
203        );
204    }
205
206    // ── compute_coverage tests ──
207
208    #[test]
209    fn all_exact() {
210        let steps = parse_steps(SIMPLE_ALGO);
211        let vals = vec![
212            fake_validation(vec![1], MatchResult::Exact),
213            fake_validation(vec![2], MatchResult::Exact),
214            fake_validation(vec![3], MatchResult::Exact),
215        ];
216        let cov = compute_coverage(&vals, &steps, "test");
217        assert_eq!(cov.total_steps, 3);
218        assert_eq!(cov.implemented_count(), 3);
219        assert!(cov.missing.is_empty());
220        assert_eq!(cov.warnings, 0);
221        assert_eq!(cov.reordered, 0);
222    }
223
224    #[test]
225    fn partial_coverage() {
226        let steps = parse_steps(SIMPLE_ALGO);
227        let vals = vec![
228            fake_validation(vec![1], MatchResult::Exact),
229            fake_validation(vec![3], MatchResult::Fuzzy),
230        ];
231        let cov = compute_coverage(&vals, &steps, "test");
232        assert_eq!(cov.total_steps, 3);
233        assert_eq!(cov.implemented_count(), 2);
234        assert_eq!(cov.missing, vec![vec![2u32]]);
235        assert_eq!(cov.warnings, 0);
236    }
237
238    #[test]
239    fn mismatch_counts_as_implemented_with_warning() {
240        let steps = parse_steps(SIMPLE_ALGO);
241        let vals = vec![
242            fake_validation(vec![1], MatchResult::Exact),
243            fake_validation(vec![2], MatchResult::Mismatch),
244        ];
245        let cov = compute_coverage(&vals, &steps, "test");
246        assert_eq!(cov.implemented_count(), 2);
247        assert_eq!(cov.warnings, 1);
248        assert_eq!(cov.missing, vec![vec![3u32]]);
249    }
250
251    #[test]
252    fn not_found_is_warning_only() {
253        let steps = parse_steps(SIMPLE_ALGO);
254        let vals = vec![
255            fake_validation(vec![1], MatchResult::Exact),
256            fake_validation(vec![99], MatchResult::NotFound),
257        ];
258        let cov = compute_coverage(&vals, &steps, "test");
259        assert_eq!(cov.implemented_count(), 1);
260        assert_eq!(cov.warnings, 1);
261        assert_eq!(cov.missing.len(), 2);
262    }
263
264    #[test]
265    fn reordered_detection() {
266        let steps = parse_steps(SIMPLE_ALGO);
267        let vals = vec![
268            fake_validation(vec![3], MatchResult::Exact),
269            fake_validation(vec![1], MatchResult::Exact),
270            fake_validation(vec![2], MatchResult::Exact),
271        ];
272        let cov = compute_coverage(&vals, &steps, "test");
273        assert_eq!(cov.implemented_count(), 3);
274        assert_eq!(cov.reordered, 1);
275    }
276
277    #[test]
278    fn no_validations() {
279        let steps = parse_steps(SIMPLE_ALGO);
280        let cov = compute_coverage(&[], &steps, "test");
281        assert_eq!(cov.total_steps, 3);
282        assert_eq!(cov.implemented_count(), 0);
283        assert_eq!(cov.missing.len(), 3);
284        assert_eq!(cov.warnings, 0);
285        assert_eq!(cov.reordered, 0);
286    }
287
288    #[test]
289    fn nested_coverage() {
290        let steps = parse_steps(NESTED_ALGO);
291        let vals = vec![
292            fake_validation(vec![1], MatchResult::Exact),
293            fake_validation(vec![1, 2], MatchResult::Fuzzy),
294        ];
295        let cov = compute_coverage(&vals, &steps, "test");
296        assert_eq!(cov.total_steps, 4);
297        assert_eq!(cov.implemented_count(), 2);
298        assert!(cov.missing.contains(&vec![1, 1]));
299        assert!(cov.missing.contains(&vec![2]));
300    }
301
302    #[test]
303    fn duplicate_step_counted_once() {
304        let steps = parse_steps(SIMPLE_ALGO);
305        let vals = vec![
306            fake_validation(vec![1], MatchResult::Exact),
307            fake_validation(vec![1], MatchResult::Exact),
308            fake_validation(vec![2], MatchResult::Exact),
309        ];
310        let cov = compute_coverage(&vals, &steps, "test");
311        assert_eq!(cov.implemented_count(), 2);
312        assert_eq!(cov.missing, vec![vec![3u32]]);
313    }
314
315    // ── CoverageResult summary tests ──
316
317    #[test]
318    fn summary_all_good() {
319        let cov = CoverageResult {
320            anchor: "navigate".into(),
321            total_steps: 23,
322            implemented: (1..=23).map(|i| vec![i]).collect(),
323            missing: vec![],
324            warnings: 0,
325            reordered: 0,
326        };
327        assert_eq!(cov.summary(), "navigate: 23/23 steps");
328    }
329
330    #[test]
331    fn summary_with_warnings() {
332        let cov = CoverageResult {
333            anchor: "navigate".into(),
334            total_steps: 23,
335            implemented: vec![vec![1], vec![2], vec![3]],
336            missing: (4..=23).map(|i| vec![i]).collect(),
337            warnings: 2,
338            reordered: 0,
339        };
340        assert_eq!(cov.summary(), "navigate: 3/23 steps | 2 warnings");
341    }
342
343    #[test]
344    fn summary_with_reordered() {
345        let cov = CoverageResult {
346            anchor: "navigate".into(),
347            total_steps: 10,
348            implemented: vec![vec![1], vec![2], vec![3]],
349            missing: vec![],
350            warnings: 0,
351            reordered: 1,
352        };
353        assert_eq!(cov.summary(), "navigate: 3/10 steps | 1 reordered");
354    }
355
356    #[test]
357    fn summary_with_all() {
358        let cov = CoverageResult {
359            anchor: "navigate".into(),
360            total_steps: 23,
361            implemented: vec![vec![1], vec![2]],
362            missing: vec![],
363            warnings: 1,
364            reordered: 2,
365        };
366        assert_eq!(
367            cov.summary(),
368            "navigate: 2/23 steps | 1 warning | 2 reordered"
369        );
370    }
371
372    #[test]
373    fn summary_singular_warning() {
374        let cov = CoverageResult {
375            anchor: "test".into(),
376            total_steps: 5,
377            implemented: vec![vec![1]],
378            missing: vec![],
379            warnings: 1,
380            reordered: 0,
381        };
382        let s = cov.summary();
383        assert!(s.contains("1 warning"));
384        assert!(!s.contains("warnings"));
385    }
386}