Skip to main content

supercov_engine/
coverage_query.rs

1//! Language-neutral coverage query operators.
2//!
3//! Querying is deliberately separated from the CLI and storage container.
4//! This module accepts the frozen analyzed view and owns structural query
5//! semantics shared by every language frontend.
6
7use std::collections::{BTreeMap, BTreeSet, HashMap};
8
9use serde::{Deserialize, Serialize};
10
11use crate::interned::Id;
12use crate::{
13    agent_json::pagination,
14    coverage_analysis::{CoverageSummary, is_independence_pair},
15    coverage_index::{
16        CoverageDimension, CoverageIndex, CoverageIndexError, CoverageViewId, IndexedCoverageModel,
17        IndexedDecisionGap, IndexedDimensionCoverage, IndexedFileGap, IndexedGapDimensions,
18        IndexedHitMetadata, IndexedMeasurement, IndexedOutcomeCounts, IndexedScopeEntry,
19        IndexedSourceScope, IndexedSummaryConfidence, IndexedTestSummary,
20    },
21    coverage_report::{
22        CoverageConfidence, CoverageReportRequest, CoverageView, DecisionMeta, ReportError,
23        SourceLine, TestAttempt, TestProvenance, TransportStats, analyze_coverage_results,
24        coverage_summary_for_tests,
25    },
26};
27use supercov_contracts::AgentPagination;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum MinimizeMetric {
32    All,
33    Lines,
34    Statements,
35    Functions,
36    Branches,
37    Mcdc,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct MinimumTestSetResult {
43    pub optimal: bool,
44    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
45    pub target: f64,
46    pub metric: MinimizeMetric,
47    pub selected: Vec<String>,
48    pub expanded: Vec<String>,
49    pub summary: CoverageSummary,
50    pub explored_states: usize,
51}
52
53#[derive(Debug, Clone, PartialEq, Deserialize)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55pub struct MinimumTestSetRequest {
56    pub coverage: CoverageReportRequest,
57    #[serde(default = "default_target")]
58    pub target: f64,
59    #[serde(default = "default_metric")]
60    pub metric: MinimizeMetric,
61    #[serde(default = "default_max_states")]
62    pub max_states: usize,
63}
64
65fn default_target() -> f64 {
66    100.0
67}
68
69fn default_metric() -> MinimizeMetric {
70    MinimizeMetric::All
71}
72
73fn default_max_states() -> usize {
74    5_000
75}
76
77pub fn minimum_test_set_for_request(
78    request: &MinimumTestSetRequest,
79) -> Result<MinimumTestSetResult, QueryError> {
80    let report = analyze_coverage_results(&request.coverage)?;
81    minimum_test_set(
82        &report.view,
83        request.target,
84        request.metric,
85        request.max_states,
86    )
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
90pub struct CoverageMinimizedTest {
91    pub id: String,
92    pub name: String,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub file: Option<String>,
95    pub runner: String,
96    pub kind: String,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize)]
100#[serde(rename_all = "camelCase")]
101pub struct CoverageMinimizeData {
102    pub run: String,
103    pub filters: CoverageQueryFilters,
104    pub optimal: bool,
105    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
106    pub target: f64,
107    pub metric: MinimizeMetric,
108    pub selected: Vec<String>,
109    pub expanded: Vec<String>,
110    pub summary: CoverageSummary,
111    pub explored_states: usize,
112    pub selected_count: usize,
113    pub total_candidate_tests: usize,
114    pub tests: Vec<CoverageMinimizedTest>,
115}
116
117#[derive(Debug, Clone, Copy)]
118pub struct CoverageMinimizeQueryOptions<'a> {
119    pub run: &'a str,
120    pub view_id: CoverageViewId,
121    pub kind: Option<&'a str>,
122    pub runner: Option<&'a str>,
123    pub target: f64,
124    pub metric: MinimizeMetric,
125    pub max_states: usize,
126    pub offset: usize,
127    pub limit: usize,
128}
129
130pub fn coverage_minimize_query(
131    view: &CoverageView,
132    options: CoverageMinimizeQueryOptions<'_>,
133) -> Result<(CoverageMinimizeData, AgentPagination), QueryError> {
134    if options.limit == 0 {
135        return Err(QueryError::InvalidPagination);
136    }
137    let selected_ids = if options.kind.is_none() && options.runner.is_none() {
138        None
139    } else {
140        let ids = view
141            .tests
142            .iter()
143            .filter(|test| {
144                options.kind.is_none_or(|kind| test.provenance.kind == kind)
145                    && options
146                        .runner
147                        .is_none_or(|runner| test.provenance.runner == runner)
148            })
149            .map(|test| test.id.clone())
150            .collect::<BTreeSet<_>>();
151        if ids.is_empty() {
152            return Err(QueryError::TestFilterEmpty {
153                kind: options.kind.map(str::to_owned),
154                runner: options.runner.map(str::to_owned),
155            });
156        }
157        Some(ids)
158    };
159    let mut solver_view = view.clone();
160    if let Some(selected) = &selected_ids {
161        solver_view.tests.retain(|test| selected.contains(&test.id));
162    }
163    let minimized = minimum_test_set(
164        &solver_view,
165        options.target,
166        options.metric,
167        options.max_states,
168    )?;
169    let selected_details = minimized
170        .selected
171        .iter()
172        .map(|id| {
173            let test = view
174                .tests
175                .iter()
176                .find(|test| test.id == *id)
177                .ok_or(QueryError::InvalidRecordSelection)?;
178            Ok(CoverageMinimizedTest {
179                id: id.clone(),
180                name: test.name.clone(),
181                file: test.file.clone(),
182                runner: test.provenance.runner.clone(),
183                kind: test.provenance.kind.clone(),
184            })
185        })
186        .collect::<Result<Vec<_>, QueryError>>()?;
187    let total = selected_details.len();
188    let tests = selected_details
189        .iter()
190        .skip(options.offset)
191        .take(options.limit)
192        .cloned()
193        .collect::<Vec<_>>();
194    let returned = tests.len();
195    let total_candidate_tests = solver_view
196        .tests
197        .iter()
198        .filter(|test| test.role == "test")
199        .count();
200    Ok((
201        CoverageMinimizeData {
202            run: options.run.into(),
203            filters: query_filters(options.view_id, options.kind, options.runner),
204            optimal: minimized.optimal,
205            target: minimized.target,
206            metric: minimized.metric,
207            selected: minimized.selected,
208            expanded: minimized.expanded,
209            summary: minimized.summary,
210            explored_states: minimized.explored_states,
211            selected_count: total,
212            total_candidate_tests,
213            tests,
214        },
215        pagination(options.offset, options.limit, returned, total),
216    ))
217}
218
219#[derive(Debug)]
220pub enum QueryError {
221    InvalidTarget(f64),
222    UnattributedEvidence,
223    TargetUnreachable {
224        metric: MinimizeMetric,
225        target: f64,
226        reachable: f64,
227    },
228    ComplexityLimit {
229        candidate_tests: usize,
230        obligations: usize,
231        explored_states: usize,
232        max_states: usize,
233        target: f64,
234        metric: MinimizeMetric,
235    },
236    Analysis(ReportError),
237    Index(CoverageIndexError),
238    InvalidPagination,
239    TestFilterEmpty {
240        kind: Option<String>,
241        runner: Option<String>,
242    },
243    TestNotFound(String),
244    DecisionNotFound(String),
245    SourceNotFound(String),
246    AmbiguousSelector {
247        selector: String,
248        matches: Vec<String>,
249    },
250    InvalidRecordSelection,
251    ScopeUnavailable,
252}
253
254impl From<ReportError> for QueryError {
255    fn from(value: ReportError) -> Self {
256        Self::Analysis(value)
257    }
258}
259
260impl From<CoverageIndexError> for QueryError {
261    fn from(value: CoverageIndexError) -> Self {
262        Self::Index(value)
263    }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct CoverageQueryFilters {
269    pub outcome: String,
270    pub kind: Option<String>,
271    pub runner: Option<String>,
272}
273
274#[derive(Debug, Clone, PartialEq, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct CoverageFilesData {
277    pub run: String,
278    pub filters: CoverageQueryFilters,
279    pub metric: MinimizeMetric,
280    pub files: Vec<IndexedFileGap>,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize)]
284#[serde(rename_all = "camelCase")]
285pub struct CoverageGapsData {
286    pub run: String,
287    pub filters: CoverageQueryFilters,
288    pub metric: MinimizeMetric,
289    pub gaps: Vec<IndexedFileGap>,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize)]
293#[serde(rename_all = "camelCase")]
294pub struct CoverageKindsData {
295    pub run: String,
296    pub filters: CoverageQueryFilters,
297    pub kinds: Vec<IndexedDimensionCoverage>,
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize)]
301#[serde(rename_all = "camelCase")]
302pub struct CoverageRunnersData {
303    pub run: String,
304    pub filters: CoverageQueryFilters,
305    pub runners: Vec<IndexedDimensionCoverage>,
306}
307
308/// Tests by how completely their coverage is their own.
309#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
310#[serde(rename_all = "camelCase")]
311pub struct TestAttributionCounts {
312    /// Credited with exactly what they reached.
313    pub exact: usize,
314    /// Credited with some of what they reached, and no way to say how much.
315    pub partial: usize,
316    /// Credited with none of it; what they reached is in the run's totals.
317    pub run_wide: usize,
318}
319
320impl TestAttributionCounts {
321    pub fn total(&self) -> usize {
322        self.exact + self.partial + self.run_wide
323    }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
327#[serde(rename_all = "camelCase")]
328pub struct CoverageDiagnostic {
329    pub code: String,
330    pub severity: String,
331    pub message: String,
332}
333
334#[derive(Debug, Clone, PartialEq, Serialize)]
335#[serde(rename_all = "camelCase")]
336pub struct CoverageSummaryData {
337    pub test_kind_sources: BTreeMap<String, usize>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub assertion_coverage: Option<serde_json::Value>,
340    pub run: String,
341    #[serde(default)]
342    pub command: Vec<String>,
343    #[serde(default, skip_serializing_if = "Vec::is_empty")]
344    pub hints: Vec<String>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub workspace: Option<String>,
347    pub filters: CoverageQueryFilters,
348    pub model: IndexedCoverageModel,
349    pub generated_at: String,
350    pub valid: bool,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub test_exit_code: Option<i32>,
353    pub stale: bool,
354    pub stale_reasons: Vec<String>,
355    pub structurally_complete: bool,
356    pub complete: bool,
357    pub coverage: CoverageSummary,
358    pub measurement: IndexedMeasurement,
359    pub coverage_by_kind: Vec<IndexedDimensionCoverage>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub e2e_gap_context: Option<CoverageKindGapContext>,
362    pub coverage_by_runner: Vec<IndexedDimensionCoverage>,
363    pub attribution: crate::coverage_index::IndexedAttribution,
364    /// How completely each test's coverage could be credited to it.
365    ///
366    /// Counted here rather than stored, because the per-test answer is already
367    /// in the run and a total is a read of it. A run written before the field
368    /// existed reports every test as exact, which is what its records say.
369    pub test_attribution: TestAttributionCounts,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub transport: Option<TransportStats>,
372    pub diagnostics: Vec<CoverageDiagnostic>,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub confidence: Option<IndexedSummaryConfidence>,
375    pub files_with_gaps: usize,
376    pub files_with_coverage_gaps: usize,
377    pub files_with_measurement_limitations: usize,
378    pub tests: usize,
379    pub setups: usize,
380    pub test_outcomes: IndexedOutcomeCounts,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub source_scope: Option<IndexedSourceScope>,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
386#[serde(rename_all = "camelCase")]
387pub struct CoverageKindGapContext {
388    pub kind: String,
389    pub other_kinds: Vec<String>,
390    pub covered_elsewhere: IndexedGapDimensions,
391    pub uncovered_everywhere: IndexedGapDimensions,
392}
393
394#[derive(Debug, Clone)]
395pub struct CoverageSummaryQueryOptions<'a> {
396    pub run: &'a str,
397    pub view: CoverageViewId,
398    pub kind: Option<&'a str>,
399    pub runner: Option<&'a str>,
400    pub valid: bool,
401    pub test_exit_code: Option<i32>,
402    pub stale: bool,
403    pub stale_reasons: Vec<String>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
407pub struct ScopeCounts {
408    pub included: usize,
409    pub excluded: usize,
410    pub ambiguous: usize,
411}
412
413#[derive(Debug, Clone, PartialEq, Serialize)]
414#[serde(rename_all = "camelCase")]
415pub struct CoverageScopeData {
416    pub run: String,
417    pub filters: CoverageQueryFilters,
418    pub kind: String,
419    pub language: String,
420    pub model: String,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub mode: Option<String>,
423    pub roots: Vec<String>,
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub unit: Option<String>,
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub measurement_complete: Option<bool>,
428    pub counts: ScopeCounts,
429    pub measurement: IndexedMeasurement,
430    pub entries: Vec<IndexedScopeEntry>,
431}
432
433#[derive(Debug, Clone, Copy)]
434pub struct CoverageScopeQueryOptions<'a> {
435    pub run: &'a str,
436    pub view: CoverageViewId,
437    pub kind: Option<&'a str>,
438    pub runner: Option<&'a str>,
439    pub offset: usize,
440    pub limit: usize,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
444pub struct CoverageLocation {
445    pub file: String,
446    pub line: usize,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
450pub struct CoverageCoveringTest {
451    pub id: String,
452    pub name: String,
453    pub provenance: TestProvenance,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
457#[serde(rename_all = "camelCase")]
458pub struct CoverageCoveringPhase {
459    pub id: String,
460    pub kind: String,
461    pub operation: String,
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub source: Option<String>,
464    pub test: String,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub status: Option<String>,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub caused_by_phase_id: Option<String>,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
472#[serde(rename_all = "camelCase")]
473pub struct CoverageAnchor {
474    pub kind: String,
475    pub id: String,
476    pub column: usize,
477    pub covered: bool,
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub source: Option<String>,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub missing: Option<String>,
482    pub covering_tests: usize,
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub covered_conditions: Option<usize>,
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub conditions: Option<usize>,
487}
488
489#[derive(Debug, Clone, PartialEq, Serialize)]
490#[serde(rename_all = "camelCase")]
491pub struct CoverageCoversLineData {
492    pub run: String,
493    pub filters: CoverageQueryFilters,
494    pub location: CoverageLocation,
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub source: Option<String>,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub source_origin: Option<String>,
499    pub covered: bool,
500    pub confidence: CoverageConfidence,
501    pub total_tests: usize,
502    pub total_phases: usize,
503    pub total_anchored: usize,
504    pub covered_anchored: usize,
505    pub total_limitations: usize,
506    pub total_remaining: usize,
507    pub tests: Vec<CoverageCoveringTest>,
508    pub phases: Vec<CoverageCoveringPhase>,
509    pub anchored: Vec<CoverageAnchor>,
510    pub limitations: Vec<CoverageFileLimitation>,
511    pub remaining: Vec<CoverageFileObligation>,
512}
513
514#[derive(Debug, Clone, PartialEq, Serialize)]
515#[serde(rename_all = "camelCase")]
516pub struct CoverageCoversAnchorsData {
517    pub run: String,
518    pub filters: CoverageQueryFilters,
519    pub location: CoverageLocation,
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub source: Option<String>,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub source_origin: Option<String>,
524    pub line_obligation: bool,
525    pub anchored: Vec<CoverageAnchor>,
526    pub total_anchored: usize,
527    pub covered_anchored: usize,
528    pub total_limitations: usize,
529    pub limitations: Vec<CoverageFileLimitation>,
530    pub total_remaining: usize,
531    pub remaining: Vec<CoverageFileObligation>,
532    pub total_tests: usize,
533    pub tests: Vec<CoverageCoveringTest>,
534}
535
536#[derive(Debug, Clone, PartialEq, Serialize)]
537#[serde(untagged)]
538pub enum CoverageCoversData {
539    Line(CoverageCoversLineData),
540    Anchors(CoverageCoversAnchorsData),
541}
542
543#[derive(Debug, Clone, Copy)]
544pub struct CoverageCoversQueryOptions<'a> {
545    pub run: &'a str,
546    pub view: CoverageViewId,
547    pub kind: Option<&'a str>,
548    pub runner: Option<&'a str>,
549    pub file: &'a str,
550    pub line: usize,
551    pub offset: usize,
552    pub limit: usize,
553}
554
555fn query_filters(
556    view: CoverageViewId,
557    kind: Option<&str>,
558    runner: Option<&str>,
559) -> CoverageQueryFilters {
560    CoverageQueryFilters {
561        outcome: match view {
562            CoverageViewId::All => "all",
563            CoverageViewId::Passed => "passed",
564            CoverageViewId::Failed => "failed",
565        }
566        .into(),
567        kind: kind.map(str::to_owned),
568        runner: runner.map(str::to_owned),
569    }
570}
571
572fn selected_test_ids(
573    tests: &[IndexedTestSummary],
574    kind: Option<&str>,
575    runner: Option<&str>,
576) -> Result<Option<BTreeSet<String>>, QueryError> {
577    if kind.is_none() && runner.is_none() {
578        return Ok(None);
579    }
580    let selected = tests
581        .iter()
582        .filter(|test| {
583            kind.is_none_or(|kind| test.provenance.kind == kind)
584                && runner.is_none_or(|runner| test.provenance.runner == runner)
585        })
586        .map(|test| test.id.clone())
587        .collect::<BTreeSet<_>>();
588    if selected.is_empty() {
589        return Err(QueryError::TestFilterEmpty {
590            kind: kind.map(str::to_owned),
591            runner: runner.map(str::to_owned),
592        });
593    }
594    Ok(Some(selected))
595}
596
597pub fn coverage_covers_query(
598    index: &CoverageIndex<'_>,
599    options: CoverageCoversQueryOptions<'_>,
600) -> Result<(CoverageCoversData, AgentPagination), QueryError> {
601    if options.limit == 0 {
602        return Err(QueryError::InvalidPagination);
603    }
604    let tests = index.test_summaries(options.view)?;
605    let selected = selected_test_ids(&tests, options.kind, options.runner)?;
606    let selected_includes = |id: &str| selected.as_ref().is_none_or(|ids| ids.contains(id));
607    let filters = query_filters(options.view, options.kind, options.runner);
608    let location = CoverageLocation {
609        file: options.file.into(),
610        line: options.line,
611    };
612    let metadata = index
613        .hit_metadata(options.view)?
614        .into_iter()
615        .map(|value| (value.id.clone(), value))
616        .collect::<HashMap<_, _>>();
617    let decisions = index
618        .decision_details(options.view)?
619        .into_iter()
620        .map(|value| (value.meta.id.clone(), value))
621        .collect::<HashMap<_, _>>();
622    let anchors = index.anchors(options.view, options.file, options.line)?;
623    let tests_by_id = tests
624        .iter()
625        .map(|test| (test.id.clone(), test.clone()))
626        .collect::<HashMap<_, _>>();
627    let mut anchor_test_ids = anchors
628        .iter()
629        .flat_map(|anchor| anchor.tests.iter())
630        .filter(|id| selected_includes(id))
631        .cloned()
632        .collect::<BTreeSet<_>>();
633    for anchor in anchors.iter().filter(|anchor| anchor.kind == "branch") {
634        anchor_test_ids.extend(
635            metadata
636                .values()
637                .filter(|detail| detail.parent_id.as_deref() == Some(anchor.id.as_str()))
638                .flat_map(|detail| detail.tests.iter())
639                .filter(|id| selected_includes(id))
640                .cloned(),
641        );
642    }
643    let all_anchor_tests = anchor_test_ids
644        .iter()
645        .map(|id| {
646            let test = tests_by_id.get(id);
647            CoverageCoveringTest {
648                id: id.clone(),
649                name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
650                provenance: test
651                    .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
652            }
653        })
654        .collect::<Vec<_>>();
655    let total_anchor_tests = all_anchor_tests.len();
656    let anchor_tests_page = all_anchor_tests
657        .iter()
658        .skip(options.offset)
659        .take(options.limit)
660        .cloned()
661        .collect::<Vec<_>>();
662    let render_anchor = |anchor: crate::coverage_index::IndexedAnchor| {
663        let branch_alternatives = if anchor.kind == "branch" {
664            metadata
665                .values()
666                .filter(|detail| {
667                    detail.obligation == "branch"
668                        && detail.parent_id.as_deref() == Some(anchor.id.as_str())
669                })
670                .collect::<Vec<_>>()
671        } else {
672            Vec::new()
673        };
674        let branch_tests = branch_alternatives
675            .iter()
676            .flat_map(|detail| detail.tests.iter())
677            .filter(|test| selected_includes(test))
678            .cloned()
679            .collect::<BTreeSet<_>>();
680        let covering_tests = if anchor.kind == "branch" {
681            branch_tests.len()
682        } else {
683            anchor
684                .tests
685                .iter()
686                .filter(|test| selected_includes(test))
687                .count()
688        };
689        let detail = metadata.get(&anchor.id);
690        let decision = decisions
691            .get(&anchor.id)
692            .cloned()
693            .map(|decision| selected_decision(decision, selected.as_ref()));
694        let conditions = decision.as_ref().map_or(anchor.conditions, |decision| {
695            Some(decision.conditions.len())
696        });
697        let covered_conditions = decision
698            .as_ref()
699            .map_or(anchor.covered_conditions, |decision| {
700                Some(
701                    decision
702                        .conditions
703                        .iter()
704                        .filter(|condition| condition.covered)
705                        .count(),
706                )
707            });
708        let covered = match anchor.kind.as_str() {
709            "decision" => conditions == covered_conditions,
710            "branch" if !branch_alternatives.is_empty() => branch_alternatives
711                .iter()
712                .all(|detail| detail.tests.iter().any(|test| selected_includes(test))),
713            "branch" => anchor.covered,
714            _ => covering_tests > 0,
715        };
716        let branch_source = branch_alternatives
717            .first()
718            .map(|detail| detail.source.clone());
719        let missing_branch_alternatives = branch_alternatives
720            .iter()
721            .filter(|detail| !detail.tests.iter().any(|test| selected_includes(test)))
722            .filter_map(|detail| detail.alternative.clone())
723            .collect::<Vec<_>>();
724        CoverageAnchor {
725            kind: anchor.kind,
726            id: anchor.id,
727            column: anchor.column,
728            covered,
729            source: decision
730                .as_ref()
731                .map(|decision| decision.meta.source.clone())
732                .or(branch_source)
733                .or_else(|| {
734                    detail.map(|detail| {
735                        detail
736                            .label
737                            .clone()
738                            .unwrap_or_else(|| detail.source.clone())
739                    })
740                })
741                .and_then(|source| compact_source(&source)),
742            missing: if missing_branch_alternatives.is_empty() {
743                detail.and_then(|detail| detail.alternative.clone())
744            } else {
745                Some(missing_branch_alternatives.join("; "))
746            },
747            covering_tests,
748            covered_conditions,
749            conditions,
750        }
751    };
752    let rendered_anchors = anchors.into_iter().map(render_anchor).collect::<Vec<_>>();
753    let total_anchored = rendered_anchors.len();
754    let covered_anchored = rendered_anchors
755        .iter()
756        .filter(|anchor| anchor.covered)
757        .count();
758    let all_limitations = index
759        .limitations(options.view)?
760        .into_iter()
761        .filter(|limitation| limitation.file == options.file && limitation.line == options.line)
762        .map(|limitation| CoverageFileLimitation {
763            id: limitation.id,
764            kind: limitation.kind,
765            file: limitation.file,
766            line: limitation.line,
767            column: limitation.column,
768            source: limitation.source,
769            reason: limitation.reason,
770            blocking: limitation.blocking,
771            effect: "outside-measured-denominator".into(),
772        })
773        .collect::<Vec<_>>();
774    let total_limitations = all_limitations.len();
775    let limitations_page = all_limitations
776        .iter()
777        .skip(options.offset)
778        .take(options.limit)
779        .cloned()
780        .collect::<Vec<_>>();
781    let (file_detail, _) = coverage_file_detail_query(
782        index,
783        CoverageFileDetailOptions {
784            run: options.run,
785            view: options.view,
786            kind: options.kind,
787            runner: options.runner,
788            selector: options.file,
789            metric: MinimizeMetric::All,
790            offset: 0,
791            limit: usize::MAX,
792        },
793    )?;
794    let gap_line = file_detail
795        .gap_lines
796        .into_iter()
797        .find(|gap| gap.line == options.line);
798    // Covered lines are absent from the gap projection, but their anchored
799    // statement/branch/decision metadata still carries the source snippet.
800    // Prefer the exact gap-line text when present and otherwise retain that
801    // anchored source so a successful line query never contradicts itself by
802    // claiming the source is unavailable while printing it below.
803    let source = gap_line
804        .as_ref()
805        .and_then(|gap| gap.source.clone())
806        .or_else(|| {
807            rendered_anchors
808                .iter()
809                .filter_map(|anchor| anchor.source.clone())
810                .min_by_key(|source| source.len())
811        });
812    let all_remaining = gap_line.map_or_else(Vec::new, |gap| gap.obligations);
813    let total_remaining = all_remaining.len();
814    let remaining_page = all_remaining
815        .iter()
816        .skip(options.offset)
817        .take(options.limit)
818        .cloned()
819        .collect::<Vec<_>>();
820    let Some(line) = index.line(options.view, options.file, options.line)? else {
821        let anchored = rendered_anchors
822            .iter()
823            .skip(options.offset)
824            .take(options.limit)
825            .cloned()
826            .collect::<Vec<_>>();
827        let total = total_anchored.max(total_limitations).max(total_remaining);
828        let total = total.max(total_anchor_tests);
829        let returned = anchored
830            .len()
831            .max(limitations_page.len())
832            .max(remaining_page.len())
833            .max(anchor_tests_page.len());
834        return Ok((
835            CoverageCoversData::Anchors(CoverageCoversAnchorsData {
836                run: options.run.into(),
837                filters,
838                location,
839                source,
840                source_origin: None,
841                line_obligation: false,
842                anchored,
843                total_anchored,
844                covered_anchored,
845                total_limitations,
846                limitations: limitations_page,
847                total_remaining,
848                remaining: remaining_page,
849                total_tests: total_anchor_tests,
850                tests: anchor_tests_page,
851            }),
852            pagination(options.offset, options.limit, returned, total),
853        ));
854    };
855    let all_tests = line
856        .tests
857        .iter()
858        .filter(|id| selected_includes(id))
859        .map(|id| {
860            let test = tests_by_id.get(id);
861            CoverageCoveringTest {
862                id: id.clone(),
863                name: test.map_or_else(|| id.clone(), |test| test.name.clone()),
864                provenance: test
865                    .map_or_else(TestProvenance::default, |test| test.provenance.clone()),
866            }
867        })
868        .collect::<Vec<_>>();
869    let phases_by_id = index
870        .phase_summaries(options.view)?
871        .into_iter()
872        .map(|phase| (phase.id.clone(), phase))
873        .collect::<HashMap<_, _>>();
874    let all_phases = line
875        .phases
876        .iter()
877        .filter_map(|id| phases_by_id.get(id))
878        .filter(|phase| selected_includes(&phase.test))
879        .map(|phase| CoverageCoveringPhase {
880            id: phase.id.clone(),
881            kind: phase.kind.clone(),
882            operation: phase.operation.clone(),
883            source: phase.source.clone(),
884            test: phase.test.clone(),
885            status: phase.status.clone(),
886            caused_by_phase_id: phase.caused_by_phase_id.clone(),
887        })
888        .collect::<Vec<_>>();
889    let tests_page = all_tests
890        .iter()
891        .skip(options.offset)
892        .take(options.limit)
893        .cloned()
894        .collect::<Vec<_>>();
895    let phases_page = all_phases
896        .iter()
897        .skip(options.offset)
898        .take(options.limit)
899        .cloned()
900        .collect::<Vec<_>>();
901    let anchored_page = rendered_anchors
902        .into_iter()
903        .skip(options.offset)
904        .take(options.limit)
905        .collect::<Vec<_>>();
906    let total = all_tests
907        .len()
908        .max(all_phases.len())
909        .max(total_anchored)
910        .max(total_limitations)
911        .max(total_remaining);
912    let returned = tests_page
913        .len()
914        .max(phases_page.len())
915        .max(anchored_page.len())
916        .max(limitations_page.len())
917        .max(remaining_page.len());
918    Ok((
919        CoverageCoversData::Line(CoverageCoversLineData {
920            run: options.run.into(),
921            filters,
922            location,
923            source,
924            source_origin: None,
925            covered: line.tests.iter().any(|test| selected_includes(test)),
926            confidence: line.confidence,
927            total_tests: all_tests.len(),
928            total_phases: all_phases.len(),
929            total_anchored,
930            covered_anchored,
931            total_limitations,
932            total_remaining,
933            tests: tests_page,
934            phases: phases_page,
935            anchored: anchored_page,
936            limitations: limitations_page,
937            remaining: remaining_page,
938        }),
939        pagination(options.offset, options.limit, returned, total),
940    ))
941}
942
943#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
944pub struct CoverageTestMatch {
945    pub id: String,
946    pub name: String,
947    pub outcome: String,
948    pub provenance: TestProvenance,
949}
950
951#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
952#[serde(rename_all = "camelCase")]
953pub struct CoverageHitDetail {
954    pub id: String,
955    pub obligation: String,
956    #[serde(skip_serializing_if = "Option::is_none")]
957    pub branch_kind: Option<String>,
958    #[serde(skip_serializing_if = "Option::is_none")]
959    pub file: Option<String>,
960    #[serde(skip_serializing_if = "Option::is_none")]
961    pub line: Option<usize>,
962    #[serde(skip_serializing_if = "Option::is_none")]
963    pub column: Option<usize>,
964    #[serde(skip_serializing_if = "Option::is_none")]
965    pub label: Option<String>,
966    #[serde(skip_serializing_if = "Option::is_none")]
967    pub alternative: Option<String>,
968}
969
970#[derive(Debug, Clone, PartialEq, Serialize)]
971pub struct CoverageTestDecision {
972    pub id: String,
973    pub vectors: Vec<crate::coverage_analysis::McdcVector>,
974    pub meta: DecisionMeta,
975}
976
977#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
978#[serde(rename_all = "camelCase")]
979pub struct CoverageTestPhase {
980    pub id: String,
981    pub kind: String,
982    pub operation: String,
983    #[serde(skip_serializing_if = "Option::is_none")]
984    pub source: Option<String>,
985    #[serde(skip_serializing_if = "Option::is_none")]
986    pub status: Option<String>,
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub caused_by_phase_id: Option<String>,
989    pub lines: usize,
990    pub decisions: usize,
991}
992
993#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
994pub struct CoverageTestTotals {
995    pub lines: usize,
996    pub hits: usize,
997    pub decisions: usize,
998    pub phases: usize,
999}
1000
1001#[derive(Debug, Clone, PartialEq, Serialize)]
1002#[serde(rename_all = "camelCase")]
1003pub struct CoverageSelectedTest {
1004    pub id: String,
1005    pub name: String,
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    pub file: Option<String>,
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub title: Option<String>,
1010    pub retries: Vec<usize>,
1011    pub attempts: Vec<TestAttempt>,
1012    pub outcome: String,
1013    pub provenance: TestProvenance,
1014    pub role: String,
1015    /// `exact`, or `run-wide` when this test ran and nothing can say what it
1016    /// reached. Under `run-wide` the totals below are zero because the test
1017    /// has no coverage of its own, not because it reached nothing.
1018    pub attribution: String,
1019    pub hits: Vec<String>,
1020    pub decisions: Vec<CoverageTestDecision>,
1021    pub lines: Vec<SourceLine>,
1022    pub hit_details: Vec<CoverageHitDetail>,
1023    pub phases: Vec<CoverageTestPhase>,
1024    pub totals: CoverageTestTotals,
1025}
1026
1027#[derive(Debug, Clone, PartialEq, Serialize)]
1028pub struct CoverageTestMatchesData {
1029    pub run: String,
1030    pub filters: CoverageQueryFilters,
1031    pub tests: Vec<CoverageTestMatch>,
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Serialize)]
1035#[serde(rename_all = "camelCase")]
1036pub struct CoverageTestDetailData {
1037    pub run: String,
1038    pub filters: CoverageQueryFilters,
1039    pub pagination_applies_to: String,
1040    pub tests: Vec<CoverageSelectedTest>,
1041}
1042
1043#[derive(Debug, Clone, PartialEq, Serialize)]
1044#[serde(untagged)]
1045pub enum CoverageTestData {
1046    Matches(CoverageTestMatchesData),
1047    Detail(CoverageTestDetailData),
1048}
1049
1050#[derive(Debug, Clone, Copy)]
1051pub struct CoverageTestQueryOptions<'a> {
1052    pub run: &'a str,
1053    pub view: CoverageViewId,
1054    pub kind: Option<&'a str>,
1055    pub runner: Option<&'a str>,
1056    pub selector: &'a str,
1057    pub offset: usize,
1058    pub limit: usize,
1059}
1060
1061fn hit_detail(id: &str, metadata: Option<&IndexedHitMetadata>) -> CoverageHitDetail {
1062    match metadata {
1063        Some(metadata) => CoverageHitDetail {
1064            id: id.into(),
1065            obligation: metadata.obligation.clone(),
1066            branch_kind: metadata.branch_kind.clone(),
1067            file: Some(metadata.file.clone()),
1068            line: Some(metadata.line),
1069            column: Some(metadata.column),
1070            label: metadata.label.clone(),
1071            alternative: metadata.alternative.clone(),
1072        },
1073        None => CoverageHitDetail {
1074            id: id.into(),
1075            obligation: "unknown".into(),
1076            branch_kind: None,
1077            file: None,
1078            line: None,
1079            column: None,
1080            label: None,
1081            alternative: None,
1082        },
1083    }
1084}
1085
1086pub fn coverage_test_query(
1087    index: &CoverageIndex<'_>,
1088    options: CoverageTestQueryOptions<'_>,
1089) -> Result<(CoverageTestData, AgentPagination), QueryError> {
1090    if options.limit == 0 {
1091        return Err(QueryError::InvalidPagination);
1092    }
1093    let tests = index.test_details(options.view)?;
1094    let summaries = tests
1095        .iter()
1096        .map(|test| test.summary.clone())
1097        .collect::<Vec<_>>();
1098    let selected = selected_test_ids(&summaries, options.kind, options.runner)?;
1099    let selector = options.selector.to_lowercase();
1100    let matches = tests
1101        .into_iter()
1102        .filter(|test| {
1103            selected
1104                .as_ref()
1105                .is_none_or(|ids| ids.contains(&test.summary.id))
1106        })
1107        .filter(|test| {
1108            test.summary.id == selector || test.summary.name.to_lowercase().contains(&selector)
1109        })
1110        .collect::<Vec<_>>();
1111    if matches.is_empty() {
1112        return Err(QueryError::TestNotFound(options.selector.into()));
1113    }
1114    let filters = query_filters(options.view, options.kind, options.runner);
1115    if matches.len() > 1 {
1116        let total = matches.len();
1117        let page = matches
1118            .into_iter()
1119            .skip(options.offset)
1120            .take(options.limit)
1121            .map(|test| CoverageTestMatch {
1122                id: test.summary.id,
1123                name: test.summary.name,
1124                outcome: test.summary.outcome,
1125                provenance: test.summary.provenance,
1126            })
1127            .collect::<Vec<_>>();
1128        let returned = page.len();
1129        return Ok((
1130            CoverageTestData::Matches(CoverageTestMatchesData {
1131                run: options.run.into(),
1132                filters,
1133                tests: page,
1134            }),
1135            pagination(options.offset, options.limit, returned, total),
1136        ));
1137    }
1138    let test = matches.into_iter().next().expect("one test match");
1139    let metadata = index
1140        .hit_metadata(options.view)?
1141        .into_iter()
1142        .map(|metadata| (metadata.id.clone(), metadata))
1143        .collect::<HashMap<_, _>>();
1144    let decisions = index
1145        .decision_metadata(options.view)?
1146        .into_iter()
1147        .map(|decision| (decision.id.clone(), decision))
1148        .collect::<HashMap<_, _>>();
1149    let all_phases = index
1150        .phase_summaries(options.view)?
1151        .into_iter()
1152        .filter(|phase| phase.test == test.summary.id)
1153        .map(|phase| CoverageTestPhase {
1154            id: phase.id,
1155            kind: phase.kind,
1156            operation: phase.operation,
1157            source: phase.source,
1158            status: phase.status,
1159            caused_by_phase_id: phase.caused_by_phase_id,
1160            lines: phase.lines,
1161            decisions: phase.decisions,
1162        })
1163        .collect::<Vec<_>>();
1164    let totals = CoverageTestTotals {
1165        lines: test.lines.len(),
1166        hits: test.hits.len(),
1167        decisions: test.decisions.len(),
1168        phases: all_phases.len(),
1169    };
1170    let total = totals
1171        .lines
1172        .max(totals.hits)
1173        .max(totals.decisions)
1174        .max(totals.phases);
1175    let lines = test
1176        .lines
1177        .iter()
1178        .skip(options.offset)
1179        .take(options.limit)
1180        .cloned()
1181        .collect::<Vec<_>>();
1182    let hits = test
1183        .hits
1184        .iter()
1185        .skip(options.offset)
1186        .take(options.limit)
1187        .cloned()
1188        .collect::<Vec<_>>();
1189    let hit_details = test
1190        .hits
1191        .iter()
1192        .skip(options.offset)
1193        .take(options.limit)
1194        .map(|id| hit_detail(id, metadata.get(id)))
1195        .collect::<Vec<_>>();
1196    let test_decisions = test
1197        .decisions
1198        .iter()
1199        .skip(options.offset)
1200        .take(options.limit)
1201        .map(|decision| {
1202            Ok(CoverageTestDecision {
1203                id: decision.id.clone(),
1204                vectors: decision.vectors.clone(),
1205                meta: decisions
1206                    .get(&decision.id)
1207                    .cloned()
1208                    .ok_or(QueryError::InvalidRecordSelection)?,
1209            })
1210        })
1211        .collect::<Result<Vec<_>, QueryError>>()?;
1212    let phases = all_phases
1213        .into_iter()
1214        .skip(options.offset)
1215        .take(options.limit)
1216        .collect::<Vec<_>>();
1217    let returned = lines
1218        .len()
1219        .max(hits.len())
1220        .max(test_decisions.len())
1221        .max(phases.len());
1222    Ok((
1223        CoverageTestData::Detail(CoverageTestDetailData {
1224            run: options.run.into(),
1225            filters,
1226            pagination_applies_to:
1227                "lines, hits/hitDetails, decisions, and phases independently within the test".into(),
1228            tests: vec![CoverageSelectedTest {
1229                id: test.summary.id,
1230                name: test.summary.name,
1231                file: test.summary.file,
1232                title: test.summary.title,
1233                retries: test.retries,
1234                attempts: test.attempts,
1235                outcome: test.summary.outcome,
1236                provenance: test.summary.provenance,
1237                role: test.summary.role,
1238                attribution: test.summary.attribution,
1239                hits,
1240                decisions: test_decisions,
1241                lines,
1242                hit_details,
1243                phases,
1244                totals,
1245            }],
1246        }),
1247        pagination(options.offset, options.limit, returned, total),
1248    ))
1249}
1250
1251#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1252pub struct CoverageDecisionMatch {
1253    pub id: String,
1254    pub file: String,
1255    pub line: usize,
1256    pub column: usize,
1257    pub source: String,
1258}
1259
1260#[derive(Debug, Clone, PartialEq, Serialize)]
1261#[serde(rename_all = "camelCase")]
1262pub struct CoverageDecisionCondition {
1263    pub index: usize,
1264    pub source: String,
1265    pub covered: bool,
1266    #[serde(skip_serializing_if = "Option::is_none")]
1267    pub assertion_covered: Option<bool>,
1268    #[serde(skip_serializing_if = "Option::is_none")]
1269    pub witness: Option<[crate::coverage_analysis::McdcVector; 2]>,
1270    #[serde(skip_serializing_if = "Option::is_none")]
1271    pub witness_tests: Option<[Vec<Id>; 2]>,
1272}
1273
1274#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1275#[serde(rename_all = "camelCase")]
1276pub struct CoverageDecisionTotals {
1277    pub conditions: usize,
1278    pub vector_observations: usize,
1279    pub tests: usize,
1280}
1281
1282#[derive(Debug, Clone, PartialEq, Serialize)]
1283#[serde(rename_all = "camelCase")]
1284pub struct CoverageSelectedDecision {
1285    pub meta: DecisionMeta,
1286    pub executed: bool,
1287    pub covered: bool,
1288    pub vectors: Vec<crate::coverage_analysis::McdcVector>,
1289    pub vector_observations: Vec<crate::coverage_report::VectorObservation>,
1290    pub conditions: Vec<CoverageDecisionCondition>,
1291    pub tests: Vec<Id>,
1292    pub confidence: CoverageConfidence,
1293    pub totals: CoverageDecisionTotals,
1294}
1295
1296#[derive(Debug, Clone, PartialEq, Serialize)]
1297pub struct CoverageDecisionMatchesData {
1298    pub run: String,
1299    pub filters: CoverageQueryFilters,
1300    pub decisions: Vec<CoverageDecisionMatch>,
1301}
1302
1303#[derive(Debug, Clone, PartialEq, Serialize)]
1304#[serde(rename_all = "camelCase")]
1305pub struct CoverageDecisionDetailData {
1306    pub run: String,
1307    pub filters: CoverageQueryFilters,
1308    pub pagination_applies_to: String,
1309    pub decisions: Vec<CoverageSelectedDecision>,
1310}
1311
1312#[derive(Debug, Clone, PartialEq, Serialize)]
1313#[serde(untagged)]
1314pub enum CoverageDecisionData {
1315    Matches(CoverageDecisionMatchesData),
1316    Detail(CoverageDecisionDetailData),
1317}
1318
1319#[derive(Debug, Clone, Copy)]
1320pub struct CoverageDecisionQueryOptions<'a> {
1321    pub run: &'a str,
1322    pub view: CoverageViewId,
1323    pub kind: Option<&'a str>,
1324    pub runner: Option<&'a str>,
1325    pub selector: &'a str,
1326    pub offset: usize,
1327    pub limit: usize,
1328}
1329
1330fn selector_location(selector: &str) -> Option<(&str, usize)> {
1331    let (prefix, last) = selector.rsplit_once(':')?;
1332    let last = last.parse::<usize>().ok()?;
1333    if let Some((file, possible_line)) = prefix.rsplit_once(':')
1334        && let Ok(line) = possible_line.parse::<usize>()
1335    {
1336        return Some((file, line));
1337    }
1338    Some((prefix, last))
1339}
1340
1341fn selected_decision(
1342    decision: crate::coverage_report::DecisionResult,
1343    selected: Option<&BTreeSet<String>>,
1344) -> crate::coverage_report::DecisionResult {
1345    let Some(selected) = selected else {
1346        return decision;
1347    };
1348    let vector_observations = decision
1349        .vector_observations
1350        .into_iter()
1351        .filter_map(|mut observation| {
1352            observation
1353                .tests
1354                .retain(|test| selected.contains(test.as_str()));
1355            (!observation.tests.is_empty()).then_some(observation)
1356        })
1357        .collect::<Vec<_>>();
1358    let vectors = vector_observations
1359        .iter()
1360        .map(|observation| observation.vector.clone())
1361        .collect::<Vec<_>>();
1362    let conditions = decision
1363        .meta
1364        .conditions
1365        .iter()
1366        .enumerate()
1367        .map(|(index, source)| {
1368            let mut witness = None;
1369            let mut witness_tests = None;
1370            'pairs: for left in 0..vector_observations.len() {
1371                for right in (left + 1)..vector_observations.len() {
1372                    let first = &vector_observations[left];
1373                    let second = &vector_observations[right];
1374                    if is_independence_pair(&first.vector, &second.vector, index) {
1375                        witness = Some([first.vector.clone(), second.vector.clone()]);
1376                        witness_tests = Some([first.tests.clone(), second.tests.clone()]);
1377                        break 'pairs;
1378                    }
1379                }
1380            }
1381            crate::coverage_report::ConditionResult {
1382                index,
1383                source: source.clone(),
1384                covered: witness.is_some(),
1385                assertion_covered: false,
1386                witness,
1387                witness_tests,
1388            }
1389        })
1390        .collect::<Vec<_>>();
1391    crate::coverage_report::DecisionResult {
1392        meta: decision.meta,
1393        executed: !vectors.is_empty(),
1394        covered: conditions.iter().all(|condition| condition.covered),
1395        vectors,
1396        vector_observations,
1397        conditions,
1398        tests: decision
1399            .tests
1400            .into_iter()
1401            .filter(|test| selected.contains(test.as_str()))
1402            .collect(),
1403        confidence: decision.confidence,
1404    }
1405}
1406
1407/// Reconstruct the exact decision view used by provenance-filtered queries.
1408/// Project filters are applied against the immutable query index.
1409pub fn filtered_decisions(
1410    index: &CoverageIndex<'_>,
1411    view: CoverageViewId,
1412    kind: Option<&str>,
1413    runner: Option<&str>,
1414) -> Result<Vec<crate::coverage_report::DecisionResult>, QueryError> {
1415    let tests = index.test_summaries(view)?;
1416    let selected = selected_test_ids(&tests, kind, runner)?;
1417    index
1418        .decision_details(view)?
1419        .into_iter()
1420        .map(|decision| Ok(selected_decision(decision, selected.as_ref())))
1421        .collect()
1422}
1423
1424pub fn coverage_decision_query(
1425    index: &CoverageIndex<'_>,
1426    options: CoverageDecisionQueryOptions<'_>,
1427) -> Result<(CoverageDecisionData, AgentPagination), QueryError> {
1428    if options.limit == 0 {
1429        return Err(QueryError::InvalidPagination);
1430    }
1431    let tests = index.test_summaries(options.view)?;
1432    let selected = selected_test_ids(&tests, options.kind, options.runner)?;
1433    let decisions = index.decision_details(options.view)?;
1434    let mut matches = decisions
1435        .into_iter()
1436        .filter(|decision| decision.meta.id == options.selector)
1437        .collect::<Vec<_>>();
1438    if matches.is_empty()
1439        && let Some((file, line)) = selector_location(options.selector)
1440    {
1441        matches = index
1442            .decision_details(options.view)?
1443            .into_iter()
1444            .filter(|decision| decision.meta.file == file && decision.meta.line == line)
1445            .collect();
1446    }
1447    if matches.is_empty() {
1448        return Err(QueryError::DecisionNotFound(options.selector.into()));
1449    }
1450    let filters = query_filters(options.view, options.kind, options.runner);
1451    if matches.len() > 1 {
1452        let total = matches.len();
1453        let page = matches
1454            .into_iter()
1455            .skip(options.offset)
1456            .take(options.limit)
1457            .map(|decision| CoverageDecisionMatch {
1458                id: decision.meta.id,
1459                file: decision.meta.file,
1460                line: decision.meta.line,
1461                column: decision.meta.column,
1462                source: decision.meta.source,
1463            })
1464            .collect::<Vec<_>>();
1465        let returned = page.len();
1466        return Ok((
1467            CoverageDecisionData::Matches(CoverageDecisionMatchesData {
1468                run: options.run.into(),
1469                filters,
1470                decisions: page,
1471            }),
1472            pagination(options.offset, options.limit, returned, total),
1473        ));
1474    }
1475    let filtered = selected_decision(
1476        matches.into_iter().next().expect("one decision match"),
1477        selected.as_ref(),
1478    );
1479    let totals = CoverageDecisionTotals {
1480        conditions: filtered.conditions.len(),
1481        vector_observations: filtered.vector_observations.len(),
1482        tests: filtered.tests.len(),
1483    };
1484    let total = totals
1485        .conditions
1486        .max(totals.vector_observations)
1487        .max(totals.tests);
1488    let vector_observations = filtered
1489        .vector_observations
1490        .iter()
1491        .skip(options.offset)
1492        .take(options.limit)
1493        .cloned()
1494        .collect::<Vec<_>>();
1495    let vectors = vector_observations
1496        .iter()
1497        .map(|observation| observation.vector.clone())
1498        .collect::<Vec<_>>();
1499    let conditions = filtered
1500        .conditions
1501        .iter()
1502        .skip(options.offset)
1503        .take(options.limit)
1504        .map(|condition| CoverageDecisionCondition {
1505            index: condition.index,
1506            source: condition.source.clone(),
1507            covered: condition.covered,
1508            assertion_covered: selected.is_none().then_some(condition.assertion_covered),
1509            witness: condition.witness.clone(),
1510            witness_tests: condition.witness_tests.clone(),
1511        })
1512        .collect::<Vec<_>>();
1513    let tests = filtered
1514        .tests
1515        .iter()
1516        .skip(options.offset)
1517        .take(options.limit)
1518        .cloned()
1519        .collect::<Vec<_>>();
1520    let returned = vector_observations
1521        .len()
1522        .max(conditions.len())
1523        .max(tests.len());
1524    Ok((
1525        CoverageDecisionData::Detail(CoverageDecisionDetailData {
1526            run: options.run.into(),
1527            filters,
1528            pagination_applies_to:
1529                "conditions, vectorObservations, and tests independently within each decision"
1530                    .into(),
1531            decisions: vec![CoverageSelectedDecision {
1532                meta: filtered.meta,
1533                executed: filtered.executed,
1534                covered: filtered.covered,
1535                vectors,
1536                vector_observations,
1537                conditions,
1538                tests,
1539                confidence: filtered.confidence,
1540                totals,
1541            }],
1542        }),
1543        pagination(options.offset, options.limit, returned, total),
1544    ))
1545}
1546
1547#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1548pub struct CoverageOtherTest {
1549    pub id: String,
1550    pub name: String,
1551}
1552
1553#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1554#[serde(rename_all = "camelCase")]
1555pub struct CoverageOtherCoverage {
1556    pub covered_elsewhere: bool,
1557    pub kinds: Vec<String>,
1558    pub runners: Vec<String>,
1559    pub tests: Vec<CoverageOtherTest>,
1560}
1561
1562#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1563#[serde(rename_all = "camelCase")]
1564pub struct CoverageLineObligation {
1565    pub kind: String,
1566    pub id: String,
1567    pub line: usize,
1568    pub other_coverage: CoverageOtherCoverage,
1569}
1570
1571#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1572#[serde(rename_all = "camelCase")]
1573pub struct CoveragePointObligation {
1574    pub kind: String,
1575    pub id: String,
1576    pub line: usize,
1577    pub column: usize,
1578    pub source: String,
1579    pub other_coverage: CoverageOtherCoverage,
1580}
1581
1582#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1583#[serde(rename_all = "camelCase")]
1584pub struct CoverageBranchObligation {
1585    pub kind: String,
1586    pub id: String,
1587    pub line: usize,
1588    pub column: usize,
1589    pub source: String,
1590    pub missing: String,
1591    pub other_coverage: CoverageOtherCoverage,
1592}
1593
1594#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1595#[serde(rename_all = "camelCase")]
1596pub struct CoverageMcdcObligation {
1597    pub kind: String,
1598    pub id: String,
1599    pub line: usize,
1600    pub column: usize,
1601    pub decision: String,
1602    pub missing_condition: String,
1603    #[serde(skip)]
1604    pub condition_index: usize,
1605    pub observed_vectors: Vec<String>,
1606    pub other_coverage: CoverageOtherCoverage,
1607}
1608
1609#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1610#[serde(untagged)]
1611pub enum CoverageFileObligation {
1612    Line(CoverageLineObligation),
1613    Point(CoveragePointObligation),
1614    Branch(CoverageBranchObligation),
1615    Mcdc(CoverageMcdcObligation),
1616}
1617
1618impl CoverageFileObligation {
1619    fn line(&self) -> usize {
1620        match self {
1621            Self::Line(value) => value.line,
1622            Self::Point(value) => value.line,
1623            Self::Branch(value) => value.line,
1624            Self::Mcdc(value) => value.line,
1625        }
1626    }
1627
1628    fn kind(&self) -> &str {
1629        match self {
1630            Self::Line(value) => &value.kind,
1631            Self::Point(value) => &value.kind,
1632            Self::Branch(value) => &value.kind,
1633            Self::Mcdc(value) => &value.kind,
1634        }
1635    }
1636}
1637
1638#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1639pub struct CoverageFileTest {
1640    pub id: String,
1641    pub name: String,
1642    pub provenance: TestProvenance,
1643}
1644
1645#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1646pub struct CoverageFileLimitation {
1647    pub id: String,
1648    pub kind: String,
1649    pub file: String,
1650    pub line: usize,
1651    pub column: usize,
1652    pub source: String,
1653    pub reason: String,
1654    pub blocking: bool,
1655    pub effect: String,
1656}
1657
1658#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1659#[serde(rename_all = "camelCase")]
1660pub struct CoverageFileCounts {
1661    /// Measured lines in the file, and how many the selected tests covered:
1662    /// the file's own line coverage, comparable with other tools'.
1663    pub total_lines: usize,
1664    pub covered_lines: usize,
1665    pub uncovered_lines: usize,
1666    pub uncovered_statements: usize,
1667    pub uncovered_functions: usize,
1668    pub missing_branches: usize,
1669    pub missing_mcdc_conditions: usize,
1670    pub measurement_limitations: usize,
1671}
1672
1673#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1674#[serde(rename_all = "camelCase")]
1675pub struct CoverageFileGapLine {
1676    pub line: usize,
1677    pub state: String,
1678    #[serde(skip_serializing_if = "Option::is_none")]
1679    pub source: Option<String>,
1680    pub obligations: Vec<CoverageFileObligation>,
1681    pub limitations: Vec<CoverageFileLimitation>,
1682}
1683
1684#[derive(Debug, Clone, PartialEq, Serialize)]
1685#[serde(rename_all = "camelCase")]
1686pub struct CoverageFileDetailData {
1687    pub run: String,
1688    pub filters: CoverageQueryFilters,
1689    pub file: String,
1690    pub metric: MinimizeMetric,
1691    pub counts: CoverageFileCounts,
1692    pub total_tests: usize,
1693    pub total_obligations: usize,
1694    pub total_gap_lines: usize,
1695    pub gap_lines: Vec<CoverageFileGapLine>,
1696    pub total_limitations: usize,
1697}
1698
1699#[derive(Debug, Clone, Copy)]
1700pub struct CoverageFileDetailOptions<'a> {
1701    pub run: &'a str,
1702    pub view: CoverageViewId,
1703    pub kind: Option<&'a str>,
1704    pub runner: Option<&'a str>,
1705    pub selector: &'a str,
1706    pub metric: MinimizeMetric,
1707    pub offset: usize,
1708    pub limit: usize,
1709}
1710
1711fn other_coverage<T: AsRef<str>>(
1712    test_ids: &[T],
1713    selected: Option<&BTreeSet<String>>,
1714    tests: &HashMap<String, IndexedTestSummary>,
1715) -> CoverageOtherCoverage {
1716    let covered = selected.map_or_else(Vec::new, |selected| {
1717        test_ids
1718            .iter()
1719            .filter(|id| !selected.contains(id.as_ref()))
1720            .filter_map(|id| tests.get(id.as_ref()))
1721            .collect::<Vec<_>>()
1722    });
1723    CoverageOtherCoverage {
1724        covered_elsewhere: !covered.is_empty(),
1725        kinds: covered
1726            .iter()
1727            .map(|test| test.provenance.kind.clone())
1728            .collect::<BTreeSet<_>>()
1729            .into_iter()
1730            .collect(),
1731        runners: covered
1732            .iter()
1733            .map(|test| test.provenance.runner.clone())
1734            .collect::<BTreeSet<_>>()
1735            .into_iter()
1736            .collect(),
1737        tests: covered
1738            .into_iter()
1739            .map(|test| CoverageOtherTest {
1740                id: test.id.clone(),
1741                name: test.name.clone(),
1742            })
1743            .collect(),
1744    }
1745}
1746
1747fn vector_text(vector: &crate::coverage_analysis::McdcVector) -> String {
1748    let values = vector
1749        .values
1750        .iter()
1751        .map(|value| match value {
1752            None => '-',
1753            Some(false) => 'F',
1754            Some(true) => 'T',
1755        })
1756        .collect::<String>();
1757    format!("{values} -> {}", if vector.outcome { 'T' } else { 'F' })
1758}
1759
1760fn obligation_matches_metric(obligation: &CoverageFileObligation, metric: MinimizeMetric) -> bool {
1761    metric == MinimizeMetric::All
1762        || matches!(
1763            (obligation.kind(), metric),
1764            ("line", MinimizeMetric::Lines)
1765                | ("statement", MinimizeMetric::Statements)
1766                | ("function", MinimizeMetric::Functions)
1767                | ("branch", MinimizeMetric::Branches)
1768                | ("mcdc", MinimizeMetric::Mcdc)
1769        )
1770}
1771
1772fn compact_source(value: &str) -> Option<String> {
1773    let line = value.lines().find(|line| !line.trim().is_empty())?.trim();
1774    let compact = line.split_whitespace().collect::<Vec<_>>().join(" ");
1775    if compact.is_empty() {
1776        None
1777    } else if compact.chars().count() > 120 {
1778        Some(format!(
1779            "{}…",
1780            compact.chars().take(119).collect::<String>()
1781        ))
1782    } else {
1783        Some(compact)
1784    }
1785}
1786
1787fn obligation_source(obligation: &CoverageFileObligation) -> Option<String> {
1788    match obligation {
1789        CoverageFileObligation::Line(_) => None,
1790        CoverageFileObligation::Point(value) => compact_source(&value.source),
1791        CoverageFileObligation::Branch(value) => compact_source(&value.source),
1792        CoverageFileObligation::Mcdc(value) => compact_source(&value.decision),
1793    }
1794}
1795
1796pub fn coverage_file_detail_query(
1797    index: &CoverageIndex<'_>,
1798    options: CoverageFileDetailOptions<'_>,
1799) -> Result<(CoverageFileDetailData, AgentPagination), QueryError> {
1800    if options.limit == 0 {
1801        return Err(QueryError::InvalidPagination);
1802    }
1803    let test_details = index.test_details(options.view)?;
1804    let test_summaries = test_details
1805        .iter()
1806        .map(|test| test.summary.clone())
1807        .collect::<Vec<_>>();
1808    let selected = selected_test_ids(&test_summaries, options.kind, options.runner)?;
1809    let tests_by_id = test_summaries
1810        .into_iter()
1811        .map(|test| (test.id.clone(), test))
1812        .collect::<HashMap<_, _>>();
1813    let lines = index.lines(options.view)?;
1814    let limitation_records = index.limitations(options.view)?;
1815    let files = lines
1816        .iter()
1817        .map(|line| line.file.as_str())
1818        .chain(
1819            limitation_records
1820                .iter()
1821                .map(|limitation| limitation.file.as_str()),
1822        )
1823        .collect::<BTreeSet<_>>();
1824    let file = if files.contains(options.selector) {
1825        options.selector.to_owned()
1826    } else {
1827        let matches = files
1828            .into_iter()
1829            .filter(|file| file.contains(options.selector))
1830            .collect::<Vec<_>>();
1831        if matches.is_empty() {
1832            return Err(QueryError::SourceNotFound(options.selector.into()));
1833        }
1834        if matches.len() != 1 {
1835            return Err(QueryError::AmbiguousSelector {
1836                selector: options.selector.into(),
1837                matches: matches.into_iter().map(str::to_owned).collect(),
1838            });
1839        }
1840        matches[0].to_owned()
1841    };
1842    let selected_includes = |tests: &[String]| {
1843        selected.as_ref().map_or(!tests.is_empty(), |selected| {
1844            tests.iter().any(|test| selected.contains(test))
1845        })
1846    };
1847    let total_lines = lines
1848        .iter()
1849        .filter(|line| line.measured && line.file == file)
1850        .count();
1851    let uncovered_lines = lines
1852        .iter()
1853        .filter(|line| line.measured && line.file == file && !selected_includes(&line.tests))
1854        .map(|line| {
1855            CoverageFileObligation::Line(CoverageLineObligation {
1856                kind: "line".into(),
1857                id: format!("line:{}:{}", line.file, line.line),
1858                line: line.line,
1859                other_coverage: other_coverage(&line.tests, selected.as_ref(), &tests_by_id),
1860            })
1861        })
1862        .collect::<Vec<_>>();
1863    let metadata = index.hit_metadata(options.view)?;
1864    let statements = metadata
1865        .iter()
1866        .filter(|point| {
1867            point.file == file
1868                && point.obligation == "statement"
1869                && !selected_includes(&point.tests)
1870        })
1871        .map(|point| {
1872            CoverageFileObligation::Point(CoveragePointObligation {
1873                kind: "statement".into(),
1874                id: point.id.clone(),
1875                line: point.line,
1876                column: point.column,
1877                source: point.label.clone().unwrap_or_else(|| point.source.clone()),
1878                other_coverage: other_coverage(&point.tests, selected.as_ref(), &tests_by_id),
1879            })
1880        })
1881        .collect::<Vec<_>>();
1882    let functions = metadata
1883        .iter()
1884        .filter(|point| {
1885            point.file == file && point.obligation == "function" && !selected_includes(&point.tests)
1886        })
1887        .map(|point| {
1888            CoverageFileObligation::Point(CoveragePointObligation {
1889                kind: "function".into(),
1890                id: point.id.clone(),
1891                line: point.line,
1892                column: point.column,
1893                source: point.label.clone().unwrap_or_else(|| point.source.clone()),
1894                other_coverage: other_coverage(&point.tests, selected.as_ref(), &tests_by_id),
1895            })
1896        })
1897        .collect::<Vec<_>>();
1898    let branches = metadata
1899        .iter()
1900        .filter(|branch| {
1901            branch.file == file
1902                && branch.obligation == "branch"
1903                && !selected_includes(&branch.tests)
1904        })
1905        .map(|branch| {
1906            CoverageFileObligation::Branch(CoverageBranchObligation {
1907                kind: "branch".into(),
1908                id: branch.id.clone(),
1909                line: branch.line,
1910                column: branch.column,
1911                source: branch.source.clone(),
1912                missing: branch.alternative.clone().unwrap_or_default(),
1913                other_coverage: other_coverage(&branch.tests, selected.as_ref(), &tests_by_id),
1914            })
1915        })
1916        .collect::<Vec<_>>();
1917    let original_decisions = index.decision_details(options.view)?;
1918    let mut mcdc = Vec::new();
1919    for original in original_decisions
1920        .iter()
1921        .filter(|decision| decision.meta.file == file)
1922    {
1923        let filtered = selected_decision(original.clone(), selected.as_ref());
1924        for condition in filtered
1925            .conditions
1926            .iter()
1927            .filter(|condition| !condition.covered)
1928        {
1929            let original_tests = original.conditions[condition.index]
1930                .witness_tests
1931                .clone()
1932                .unwrap_or_default()
1933                .into_iter()
1934                .flatten()
1935                .collect::<Vec<_>>();
1936            mcdc.push(CoverageFileObligation::Mcdc(CoverageMcdcObligation {
1937                kind: "mcdc".into(),
1938                id: original.meta.id.clone(),
1939                line: original.meta.line,
1940                column: original.meta.column,
1941                decision: original.meta.source.clone(),
1942                missing_condition: condition.source.clone(),
1943                condition_index: condition.index,
1944                observed_vectors: filtered
1945                    .vector_observations
1946                    .iter()
1947                    .map(|observation| vector_text(&observation.vector))
1948                    .collect(),
1949                other_coverage: other_coverage(&original_tests, selected.as_ref(), &tests_by_id),
1950            }));
1951        }
1952    }
1953    let mut obligations = uncovered_lines
1954        .iter()
1955        .chain(statements.iter())
1956        .chain(functions.iter())
1957        .chain(branches.iter())
1958        .chain(mcdc.iter())
1959        .filter(|obligation| obligation_matches_metric(obligation, options.metric))
1960        .cloned()
1961        .collect::<Vec<_>>();
1962    obligations.sort_by(|left, right| {
1963        left.line()
1964            .cmp(&right.line())
1965            .then_with(|| left.kind().cmp(right.kind()))
1966    });
1967    let mut limitations = limitation_records
1968        .into_iter()
1969        .filter(|limitation| limitation.file == file)
1970        .map(|limitation| CoverageFileLimitation {
1971            id: limitation.id,
1972            kind: limitation.kind,
1973            file: limitation.file,
1974            line: limitation.line,
1975            column: limitation.column,
1976            source: limitation.source,
1977            reason: limitation.reason,
1978            blocking: limitation.blocking,
1979            effect: "outside-measured-denominator".into(),
1980        })
1981        .collect::<Vec<_>>();
1982    limitations.sort_by(|left, right| {
1983        left.line
1984            .cmp(&right.line)
1985            .then_with(|| left.column.cmp(&right.column))
1986            .then_with(|| left.id.cmp(&right.id))
1987    });
1988    let total_tests = test_details
1989        .iter()
1990        .filter(|test| {
1991            selected
1992                .as_ref()
1993                .is_none_or(|selected| selected.contains(&test.summary.id))
1994                && test.lines.iter().any(|line| line.file == file)
1995        })
1996        .count();
1997    let total_obligations = obligations.len();
1998    let total_limitations = limitations.len();
1999    let mut grouped =
2000        BTreeMap::<usize, (Vec<CoverageFileObligation>, Vec<CoverageFileLimitation>)>::new();
2001    for obligation in obligations {
2002        grouped
2003            .entry(obligation.line())
2004            .or_default()
2005            .0
2006            .push(obligation);
2007    }
2008    for limitation in limitations {
2009        grouped
2010            .entry(limitation.line)
2011            .or_default()
2012            .1
2013            .push(limitation);
2014    }
2015    let gap_lines = grouped
2016        .into_iter()
2017        .map(|(line, (obligations, limitations))| {
2018            let source = obligations.iter().find_map(obligation_source).or_else(|| {
2019                limitations
2020                    .iter()
2021                    .find_map(|value| compact_source(&value.source))
2022            });
2023            let state = if obligations
2024                .iter()
2025                .any(|value| matches!(value, CoverageFileObligation::Line(_)))
2026            {
2027                "missing"
2028            } else if !obligations.is_empty() {
2029                "part"
2030            } else {
2031                "limited"
2032            };
2033            CoverageFileGapLine {
2034                line,
2035                state: state.into(),
2036                source,
2037                obligations,
2038                limitations,
2039            }
2040        })
2041        .collect::<Vec<_>>();
2042    let total_gap_lines = gap_lines.len();
2043    let selected_gap_lines = gap_lines
2044        .into_iter()
2045        .skip(options.offset)
2046        .take(options.limit)
2047        .collect::<Vec<_>>();
2048    let returned = selected_gap_lines.len();
2049    Ok((
2050        CoverageFileDetailData {
2051            run: options.run.into(),
2052            filters: query_filters(options.view, options.kind, options.runner),
2053            file,
2054            metric: options.metric,
2055            counts: CoverageFileCounts {
2056                total_lines,
2057                covered_lines: total_lines - uncovered_lines.len(),
2058                uncovered_lines: uncovered_lines.len(),
2059                uncovered_statements: statements.len(),
2060                uncovered_functions: functions.len(),
2061                missing_branches: branches.len(),
2062                missing_mcdc_conditions: mcdc.len(),
2063                measurement_limitations: total_limitations,
2064            },
2065            total_tests,
2066            total_obligations,
2067            total_gap_lines,
2068            gap_lines: selected_gap_lines,
2069            total_limitations,
2070        },
2071        pagination(options.offset, options.limit, returned, total_gap_lines),
2072    ))
2073}
2074
2075#[derive(Debug, Clone, PartialEq, Serialize)]
2076pub struct CoverageDiffDelta {
2077    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2078    pub lines: f64,
2079    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2080    pub branches: f64,
2081    #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
2082    pub mcdc: f64,
2083}
2084
2085#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2086#[serde(rename_all = "camelCase")]
2087pub struct CoverageDiffSide {
2088    pub line_count: usize,
2089    pub branch_count: usize,
2090    pub mcdc_count: usize,
2091    pub lines: Vec<String>,
2092    pub branches: Vec<String>,
2093    pub mcdc: Vec<String>,
2094}
2095
2096#[derive(Debug, Clone, PartialEq, Serialize)]
2097pub struct CoverageDiffData {
2098    pub filters: CoverageQueryFilters,
2099    pub older: String,
2100    pub newer: String,
2101    pub delta: CoverageDiffDelta,
2102    pub gained: CoverageDiffSide,
2103    pub lost: CoverageDiffSide,
2104}
2105
2106#[derive(Debug, Clone, Copy)]
2107pub struct CoverageDiffQueryOptions<'a> {
2108    pub older_run: &'a str,
2109    pub newer_run: &'a str,
2110    pub view: CoverageViewId,
2111    pub kind: Option<&'a str>,
2112    pub runner: Option<&'a str>,
2113    pub offset: usize,
2114    pub limit: usize,
2115}
2116
2117struct DiffSnapshot {
2118    summary: CoverageSummary,
2119    lines: BTreeSet<String>,
2120    branches: HashMap<String, String>,
2121    mcdc: HashMap<String, String>,
2122}
2123
2124fn diff_snapshot(
2125    index: &CoverageIndex<'_>,
2126    view: CoverageViewId,
2127) -> Result<DiffSnapshot, QueryError> {
2128    let summary = index.summary(view)?;
2129    let lines = index
2130        .lines(view)?
2131        .into_iter()
2132        .filter(|line| line.covered)
2133        .map(|line| format!("{}:{}", line.file, line.line))
2134        .collect();
2135    let branches = index
2136        .hit_metadata(view)?
2137        .into_iter()
2138        .filter(|metadata| metadata.obligation == "branch" && !metadata.tests.is_empty())
2139        .map(|metadata| {
2140            let parent = metadata
2141                .parent_id
2142                .ok_or(QueryError::InvalidRecordSelection)?;
2143            Ok((
2144                format!("{parent}:{}", metadata.id),
2145                format!(
2146                    "{}:{} {}",
2147                    metadata.file,
2148                    metadata.line,
2149                    metadata.alternative.unwrap_or_default()
2150                ),
2151            ))
2152        })
2153        .collect::<Result<HashMap<_, _>, QueryError>>()?;
2154    let mcdc = index
2155        .decision_details(view)?
2156        .into_iter()
2157        .flat_map(|decision| {
2158            decision
2159                .conditions
2160                .into_iter()
2161                .filter(|condition| condition.covered)
2162                .map(move |condition| {
2163                    (
2164                        format!("{}:c{}", decision.meta.id, condition.index),
2165                        format!(
2166                            "{}:{} C{} {}",
2167                            decision.meta.file,
2168                            decision.meta.line,
2169                            condition.index + 1,
2170                            condition.source
2171                        ),
2172                    )
2173                })
2174        })
2175        .collect();
2176    Ok(DiffSnapshot {
2177        summary,
2178        lines,
2179        branches,
2180        mcdc,
2181    })
2182}
2183
2184fn js_string_cmp(left: &str, right: &str) -> std::cmp::Ordering {
2185    left.encode_utf16().cmp(right.encode_utf16())
2186}
2187
2188fn rounded_delta(newer: f64, older: f64) -> f64 {
2189    ((newer - older) * 100.0).round() / 100.0
2190}
2191
2192pub fn coverage_diff_query(
2193    older: &CoverageIndex<'_>,
2194    newer: &CoverageIndex<'_>,
2195    options: CoverageDiffQueryOptions<'_>,
2196) -> Result<(CoverageDiffData, AgentPagination), QueryError> {
2197    if options.limit == 0 {
2198        return Err(QueryError::InvalidPagination);
2199    }
2200    let older = diff_snapshot(older, options.view)?;
2201    let newer = diff_snapshot(newer, options.view)?;
2202    let mut gained_lines = newer
2203        .lines
2204        .difference(&older.lines)
2205        .cloned()
2206        .collect::<Vec<_>>();
2207    let mut lost_lines = older
2208        .lines
2209        .difference(&newer.lines)
2210        .cloned()
2211        .collect::<Vec<_>>();
2212    let mut gained_branches = newer
2213        .branches
2214        .iter()
2215        .filter(|(id, _)| !older.branches.contains_key(*id))
2216        .map(|(_, label)| label.clone())
2217        .collect::<Vec<_>>();
2218    let mut lost_branches = older
2219        .branches
2220        .iter()
2221        .filter(|(id, _)| !newer.branches.contains_key(*id))
2222        .map(|(_, label)| label.clone())
2223        .collect::<Vec<_>>();
2224    let mut gained_mcdc = newer
2225        .mcdc
2226        .iter()
2227        .filter(|(id, _)| !older.mcdc.contains_key(*id))
2228        .map(|(_, label)| label.clone())
2229        .collect::<Vec<_>>();
2230    let mut lost_mcdc = older
2231        .mcdc
2232        .iter()
2233        .filter(|(id, _)| !newer.mcdc.contains_key(*id))
2234        .map(|(_, label)| label.clone())
2235        .collect::<Vec<_>>();
2236    for values in [
2237        &mut gained_lines,
2238        &mut lost_lines,
2239        &mut gained_branches,
2240        &mut lost_branches,
2241        &mut gained_mcdc,
2242        &mut lost_mcdc,
2243    ] {
2244        values.sort_by(|left, right| js_string_cmp(left, right));
2245    }
2246    let total = [
2247        gained_lines.len(),
2248        gained_branches.len(),
2249        gained_mcdc.len(),
2250        lost_lines.len(),
2251        lost_branches.len(),
2252        lost_mcdc.len(),
2253    ]
2254    .into_iter()
2255    .max()
2256    .unwrap_or(0);
2257    let page = |values: &[String]| {
2258        values
2259            .iter()
2260            .skip(options.offset)
2261            .take(options.limit)
2262            .cloned()
2263            .collect::<Vec<_>>()
2264    };
2265    let gained = CoverageDiffSide {
2266        line_count: gained_lines.len(),
2267        branch_count: gained_branches.len(),
2268        mcdc_count: gained_mcdc.len(),
2269        lines: page(&gained_lines),
2270        branches: page(&gained_branches),
2271        mcdc: page(&gained_mcdc),
2272    };
2273    let lost = CoverageDiffSide {
2274        line_count: lost_lines.len(),
2275        branch_count: lost_branches.len(),
2276        mcdc_count: lost_mcdc.len(),
2277        lines: page(&lost_lines),
2278        branches: page(&lost_branches),
2279        mcdc: page(&lost_mcdc),
2280    };
2281    let returned = [
2282        gained.lines.len(),
2283        gained.branches.len(),
2284        gained.mcdc.len(),
2285        lost.lines.len(),
2286        lost.branches.len(),
2287        lost.mcdc.len(),
2288    ]
2289    .into_iter()
2290    .max()
2291    .unwrap_or(0);
2292    Ok((
2293        CoverageDiffData {
2294            filters: query_filters(options.view, options.kind, options.runner),
2295            older: options.older_run.into(),
2296            newer: options.newer_run.into(),
2297            delta: CoverageDiffDelta {
2298                lines: rounded_delta(
2299                    newer.summary.lines.percentage,
2300                    older.summary.lines.percentage,
2301                ),
2302                branches: rounded_delta(
2303                    newer.summary.branches.percentage,
2304                    older.summary.branches.percentage,
2305                ),
2306                mcdc: rounded_delta(
2307                    newer.summary.condition_coverage_pct,
2308                    older.summary.condition_coverage_pct,
2309                ),
2310            },
2311            gained,
2312            lost,
2313        },
2314        pagination(options.offset, options.limit, returned, total),
2315    ))
2316}
2317
2318pub fn coverage_scope_query(
2319    index: &CoverageIndex<'_>,
2320    options: CoverageScopeQueryOptions<'_>,
2321) -> Result<(CoverageScopeData, AgentPagination), QueryError> {
2322    if options.limit == 0 {
2323        return Err(QueryError::InvalidPagination);
2324    }
2325    let projection = index.projection(options.view, options.kind, options.runner)?;
2326    let scope = projection
2327        .source_scope
2328        .ok_or(QueryError::ScopeUnavailable)?;
2329    let mut entries = index.scope_entries(options.view)?;
2330    entries.sort_by(|left, right| {
2331        let rank = |status: &str| match status {
2332            "ambiguous" => 0,
2333            "included" => 1,
2334            "excluded" => 2,
2335            _ => 3,
2336        };
2337        rank(&left.status)
2338            .cmp(&rank(&right.status))
2339            .then_with(|| left.file.cmp(&right.file))
2340    });
2341    let total = entries.len();
2342    let selected = entries
2343        .into_iter()
2344        .skip(options.offset)
2345        .take(options.limit)
2346        .collect::<Vec<_>>();
2347    let returned = selected.len();
2348    Ok((
2349        CoverageScopeData {
2350            run: options.run.into(),
2351            filters: CoverageQueryFilters {
2352                outcome: match options.view {
2353                    CoverageViewId::All => "all",
2354                    CoverageViewId::Passed => "passed",
2355                    CoverageViewId::Failed => "failed",
2356                }
2357                .into(),
2358                kind: options.kind.map(str::to_owned),
2359                runner: options.runner.map(str::to_owned),
2360            },
2361            kind: scope.kind,
2362            language: scope.language,
2363            model: scope.model,
2364            mode: scope.mode,
2365            roots: scope.roots,
2366            unit: scope.unit,
2367            measurement_complete: scope.measurement_complete,
2368            counts: ScopeCounts {
2369                included: scope.included,
2370                excluded: scope.excluded,
2371                ambiguous: scope.ambiguous,
2372            },
2373            measurement: projection.measurement,
2374            entries: selected,
2375        },
2376        pagination(options.offset, options.limit, returned, total),
2377    ))
2378}
2379
2380pub fn coverage_summary_query(
2381    index: &CoverageIndex<'_>,
2382    options: CoverageSummaryQueryOptions<'_>,
2383) -> Result<CoverageSummaryData, QueryError> {
2384    let projection = index.projection(options.view, options.kind, options.runner)?;
2385    let mut test_kind_sources = BTreeMap::new();
2386    for test in index.test_summaries(options.view)?.iter().filter(|t| {
2387        t.role == "test"
2388            && options.kind.is_none_or(|k| t.provenance.kind == k)
2389            && options.runner.is_none_or(|r| t.provenance.runner == r)
2390    }) {
2391        *test_kind_sources
2392            .entry(test.provenance.source.clone())
2393            .or_insert(0) += 1;
2394    }
2395    let mut diagnostics = Vec::new();
2396    let mut transport_blockers = 0usize;
2397    if projection.empty_evidence_tests > 0 {
2398        diagnostics.push(CoverageDiagnostic {
2399            code: "TEST_EVIDENCE_MISSING".into(),
2400            severity: "warning".into(),
2401            message: format!(
2402                "{} test(s) recorded assertion phases but attributed zero coverage evidence; possible causes include checks of uninstrumented data, shared setup, lost async context or missing probe transport. First: {}",
2403                projection.empty_evidence_tests,
2404                projection.first_empty_evidence_test.as_deref().unwrap_or("unknown")
2405            ),
2406        });
2407    }
2408    if let Some(transport) = &projection.transport {
2409        if transport.corrupt_records > 0 {
2410            transport_blockers += 1;
2411            diagnostics.push(CoverageDiagnostic {
2412                code: "CORRUPT_EVIDENCE_RECORDS".into(),
2413                severity: "error".into(),
2414                message: format!(
2415                    "{} malformed evidence record(s) in {} file(s) were excluded; coverage is incomplete.",
2416                    transport.corrupt_records, transport.corrupt_files
2417                ),
2418            });
2419        }
2420        if transport.remote_launches > 0
2421            && transport.scoped_server_records == 0
2422            && transport.background_server_records == 0
2423            && projection.attribution.server_explicit == 0
2424            && projection.attribution.server_fallback == 0
2425        {
2426            transport_blockers += 1;
2427            diagnostics.push(CoverageDiagnostic {
2428                code: "REMOTE_SERVER_EVIDENCE_MISSING".into(),
2429                severity: "error".into(),
2430                message: "Remote launches were supervised, but neither scoped nor background server evidence returned. Supercov refuses to describe this measurement as complete.".into(),
2431            });
2432        }
2433    }
2434    let coverage_by_kind = index.dimensions(options.view, CoverageDimension::Kind)?;
2435    let coverage_by_runner = index.dimensions(options.view, CoverageDimension::Runner)?;
2436    let other_e2e_kinds = coverage_by_kind
2437        .iter()
2438        .filter(|dimension| dimension.tests > 0)
2439        .filter_map(|dimension| dimension.kind.clone())
2440        .filter(|kind| kind != "e2e")
2441        .collect::<Vec<_>>();
2442    let e2e_observed = coverage_by_kind
2443        .iter()
2444        .any(|dimension| dimension.tests > 0 && dimension.kind.as_deref() == Some("e2e"));
2445    let e2e_gap_context = if options.kind.is_none()
2446        && options.runner.is_none()
2447        && e2e_observed
2448        && !other_e2e_kinds.is_empty()
2449    {
2450        let mut covered_elsewhere = IndexedGapDimensions {
2451            lines: 0,
2452            statements: 0,
2453            functions: 0,
2454            branches: 0,
2455            mcdc_conditions: 0,
2456        };
2457        let mut uncovered_everywhere = covered_elsewhere.clone();
2458        for gap in index.file_gaps(options.view, Some("e2e"), None)? {
2459            covered_elsewhere.lines += gap.covered_by_other_tests.lines;
2460            covered_elsewhere.statements += gap.covered_by_other_tests.statements;
2461            covered_elsewhere.functions += gap.covered_by_other_tests.functions;
2462            covered_elsewhere.branches += gap.covered_by_other_tests.branches;
2463            covered_elsewhere.mcdc_conditions += gap.covered_by_other_tests.mcdc_conditions;
2464            uncovered_everywhere.lines += gap.uncovered_everywhere.lines;
2465            uncovered_everywhere.statements += gap.uncovered_everywhere.statements;
2466            uncovered_everywhere.functions += gap.uncovered_everywhere.functions;
2467            uncovered_everywhere.branches += gap.uncovered_everywhere.branches;
2468            uncovered_everywhere.mcdc_conditions += gap.uncovered_everywhere.mcdc_conditions;
2469        }
2470        Some(CoverageKindGapContext {
2471            kind: "e2e".into(),
2472            other_kinds: other_e2e_kinds,
2473            covered_elsewhere,
2474            uncovered_everywhere,
2475        })
2476    } else {
2477        None
2478    };
2479    let filters = CoverageQueryFilters {
2480        outcome: match options.view {
2481            CoverageViewId::All => "all",
2482            CoverageViewId::Passed => "passed",
2483            CoverageViewId::Failed => "failed",
2484        }
2485        .into(),
2486        kind: options.kind.map(str::to_owned),
2487        runner: options.runner.map(str::to_owned),
2488    };
2489    let mut measurement = projection.measurement.clone();
2490    if transport_blockers > 0 {
2491        measurement.complete = false;
2492        measurement.limitations = measurement.limitations.saturating_add(transport_blockers);
2493        measurement.blocking = measurement.blocking.saturating_add(transport_blockers);
2494    }
2495    let structurally_complete = projection.summary.coverage_complete && measurement.complete;
2496    let complete = options.view == CoverageViewId::Passed
2497        && options.valid
2498        && !options.stale
2499        && structurally_complete;
2500    Ok(CoverageSummaryData {
2501        test_kind_sources,
2502        assertion_coverage: None,
2503        run: options.run.into(),
2504        command: Vec::new(),
2505        hints: Vec::new(),
2506        workspace: None,
2507        filters,
2508        model: index.model()?,
2509        generated_at: projection.generated_at,
2510        valid: options.valid,
2511        test_exit_code: options.test_exit_code,
2512        stale: options.stale,
2513        stale_reasons: options.stale_reasons,
2514        structurally_complete,
2515        complete,
2516        coverage: projection.summary,
2517        measurement,
2518        coverage_by_kind,
2519        e2e_gap_context,
2520        coverage_by_runner,
2521        attribution: projection.attribution,
2522        test_attribution: {
2523            let mut counts = TestAttributionCounts::default();
2524            for test in index
2525                .test_summaries(options.view)?
2526                .iter()
2527                .filter(|test| test.role == "test")
2528            {
2529                match test.attribution.as_str() {
2530                    crate::coverage_report::ATTRIBUTION_RUN_WIDE => counts.run_wide += 1,
2531                    crate::coverage_report::ATTRIBUTION_PARTIAL => counts.partial += 1,
2532                    _ => counts.exact += 1,
2533                }
2534            }
2535            counts
2536        },
2537        transport: projection.transport,
2538        diagnostics,
2539        confidence: (options.kind.is_none() && options.runner.is_none())
2540            .then_some(projection.confidence),
2541        files_with_gaps: projection.files_with_gaps,
2542        files_with_coverage_gaps: projection.files_with_coverage_gaps,
2543        files_with_measurement_limitations: projection.measurement.files,
2544        tests: projection.tests,
2545        setups: projection.setups,
2546        test_outcomes: projection.test_outcomes,
2547        source_scope: projection.source_scope,
2548    })
2549}
2550
2551#[derive(Debug, Clone, PartialEq)]
2552pub enum CoverageDimensionQueryData {
2553    Kinds(CoverageKindsData),
2554    Runners(CoverageRunnersData),
2555}
2556
2557#[derive(Debug, Clone, PartialEq)]
2558pub enum CoverageFileQueryData {
2559    Files(CoverageFilesData),
2560    Gaps(CoverageGapsData),
2561}
2562
2563#[derive(Debug, Clone, PartialEq)]
2564pub struct CoverageFileQueryResult {
2565    pub data: CoverageFileQueryData,
2566    pub pagination: AgentPagination,
2567}
2568
2569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2570#[serde(rename_all = "lowercase")]
2571pub enum DecisionSort {
2572    Location,
2573    Missing,
2574}
2575
2576#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2577#[serde(rename_all = "camelCase")]
2578pub struct DecisionGapTotals {
2579    pub decisions: usize,
2580    pub decisions_with_missing_conditions: usize,
2581    pub conditions: usize,
2582    pub missing_conditions: usize,
2583}
2584
2585#[derive(Debug, Clone, PartialEq, Serialize)]
2586#[serde(rename_all = "camelCase")]
2587pub struct CoverageFileDecisionsData {
2588    pub run: String,
2589    pub filters: CoverageQueryFilters,
2590    pub file: String,
2591    pub group: String,
2592    pub sort: DecisionSort,
2593    pub totals: DecisionGapTotals,
2594    pub decisions: Vec<IndexedDecisionGap>,
2595}
2596
2597#[derive(Debug, Clone, Copy)]
2598pub struct CoverageFileDecisionsOptions<'a> {
2599    pub run: &'a str,
2600    pub view: CoverageViewId,
2601    pub kind: Option<&'a str>,
2602    pub runner: Option<&'a str>,
2603    pub file: &'a str,
2604    pub sort: DecisionSort,
2605    pub offset: usize,
2606    pub limit: usize,
2607}
2608
2609#[derive(Debug, Clone)]
2610pub struct CoverageDimensionQueryOptions<'a> {
2611    pub run: &'a str,
2612    pub view: CoverageViewId,
2613    pub dimension: CoverageDimension,
2614    pub filters: CoverageQueryFilters,
2615    pub offset: usize,
2616    pub limit: usize,
2617}
2618
2619pub fn coverage_dimension_query(
2620    index: &CoverageIndex<'_>,
2621    options: CoverageDimensionQueryOptions<'_>,
2622) -> Result<(CoverageDimensionQueryData, AgentPagination), QueryError> {
2623    let CoverageDimensionQueryOptions {
2624        run,
2625        view,
2626        dimension,
2627        filters,
2628        offset,
2629        limit,
2630    } = options;
2631    if limit == 0 {
2632        return Err(QueryError::InvalidPagination);
2633    }
2634    let values = index.dimensions(view, dimension)?;
2635    let total = values.len();
2636    let selected = values
2637        .into_iter()
2638        .skip(offset)
2639        .take(limit)
2640        .collect::<Vec<_>>();
2641    let returned = selected.len();
2642    let data = match dimension {
2643        CoverageDimension::Kind => CoverageDimensionQueryData::Kinds(CoverageKindsData {
2644            run: run.into(),
2645            filters,
2646            kinds: selected,
2647        }),
2648        CoverageDimension::Runner => CoverageDimensionQueryData::Runners(CoverageRunnersData {
2649            run: run.into(),
2650            filters,
2651            runners: selected,
2652        }),
2653    };
2654    Ok((data, pagination(offset, limit, returned, total)))
2655}
2656
2657#[derive(Debug, Clone, Copy)]
2658pub struct CoverageFileQueryOptions<'a> {
2659    pub run: &'a str,
2660    pub view: CoverageViewId,
2661    pub metric: MinimizeMetric,
2662    pub gaps_only: bool,
2663    pub kind: Option<&'a str>,
2664    pub runner: Option<&'a str>,
2665    pub offset: usize,
2666    pub limit: usize,
2667}
2668
2669fn gap_metric_value(gap: &IndexedFileGap, metric: MinimizeMetric) -> usize {
2670    match metric {
2671        MinimizeMetric::All => gap.score,
2672        MinimizeMetric::Lines => gap.uncovered_lines,
2673        MinimizeMetric::Statements => gap.uncovered_statements,
2674        MinimizeMetric::Functions => gap.uncovered_functions,
2675        MinimizeMetric::Branches => gap.missing_branches,
2676        MinimizeMetric::Mcdc => gap.missing_mcdc_conditions,
2677    }
2678}
2679
2680fn has_gap_for_metric(gap: &IndexedFileGap, metric: MinimizeMetric) -> bool {
2681    match metric {
2682        MinimizeMetric::All => {
2683            gap.uncovered_lines > 0
2684                || gap.uncovered_statements > 0
2685                || gap.uncovered_functions > 0
2686                || gap.missing_branches > 0
2687                || gap.missing_mcdc_conditions > 0
2688        }
2689        _ => gap_metric_value(gap, metric) > 0,
2690    }
2691}
2692
2693pub fn coverage_file_decisions_query(
2694    index: &CoverageIndex<'_>,
2695    options: CoverageFileDecisionsOptions<'_>,
2696) -> Result<(CoverageFileDecisionsData, AgentPagination), QueryError> {
2697    if options.limit == 0 {
2698        return Err(QueryError::InvalidPagination);
2699    }
2700    let all = index.decision_gaps(options.view, options.kind, options.runner, options.file)?;
2701    if (options.kind.is_some() || options.runner.is_some()) && all.is_empty() {
2702        // A file with no decisions is valid, so consult the file projection to
2703        // distinguish it from a nonexistent test provenance projection.
2704        if index
2705            .file_gaps(options.view, options.kind, options.runner)?
2706            .is_empty()
2707        {
2708            return Err(QueryError::TestFilterEmpty {
2709                kind: options.kind.map(str::to_owned),
2710                runner: options.runner.map(str::to_owned),
2711            });
2712        }
2713    }
2714    let totals = DecisionGapTotals {
2715        decisions: all.len(),
2716        decisions_with_missing_conditions: all
2717            .iter()
2718            .filter(|decision| decision.missing_conditions > 0)
2719            .count(),
2720        conditions: all.iter().map(|decision| decision.conditions).sum(),
2721        missing_conditions: all.iter().map(|decision| decision.missing_conditions).sum(),
2722    };
2723    let mut missing = all
2724        .into_iter()
2725        .filter(|decision| decision.missing_conditions > 0)
2726        .collect::<Vec<_>>();
2727    missing.sort_by(|left, right| match options.sort {
2728        DecisionSort::Missing => right
2729            .missing_conditions
2730            .cmp(&left.missing_conditions)
2731            .then_with(|| left.line.cmp(&right.line))
2732            .then_with(|| left.column.cmp(&right.column)),
2733        DecisionSort::Location => left
2734            .line
2735            .cmp(&right.line)
2736            .then_with(|| left.column.cmp(&right.column))
2737            .then_with(|| left.id.cmp(&right.id)),
2738    });
2739    let total = missing.len();
2740    let rows = missing
2741        .into_iter()
2742        .skip(options.offset)
2743        .take(options.limit)
2744        .collect::<Vec<_>>();
2745    let returned = rows.len();
2746    let filters = CoverageQueryFilters {
2747        outcome: match options.view {
2748            CoverageViewId::All => "all",
2749            CoverageViewId::Passed => "passed",
2750            CoverageViewId::Failed => "failed",
2751        }
2752        .into(),
2753        kind: options.kind.map(str::to_owned),
2754        runner: options.runner.map(str::to_owned),
2755    };
2756    Ok((
2757        CoverageFileDecisionsData {
2758            run: options.run.into(),
2759            filters,
2760            file: options.file.into(),
2761            group: "decision".into(),
2762            sort: options.sort,
2763            totals,
2764            decisions: rows,
2765        },
2766        pagination(options.offset, options.limit, returned, total),
2767    ))
2768}
2769
2770pub fn coverage_file_query(
2771    index: &CoverageIndex<'_>,
2772    options: CoverageFileQueryOptions<'_>,
2773) -> Result<CoverageFileQueryResult, QueryError> {
2774    let CoverageFileQueryOptions {
2775        run,
2776        view,
2777        metric,
2778        gaps_only,
2779        kind,
2780        runner,
2781        offset,
2782        limit,
2783    } = options;
2784    if limit == 0 {
2785        return Err(QueryError::InvalidPagination);
2786    }
2787    let mut files = index.file_gaps(view, kind, runner)?;
2788    if (kind.is_some() || runner.is_some()) && files.is_empty() {
2789        return Err(QueryError::TestFilterEmpty {
2790            kind: kind.map(str::to_owned),
2791            runner: runner.map(str::to_owned),
2792        });
2793    }
2794    if gaps_only {
2795        files.retain(|gap| has_gap_for_metric(gap, metric) || gap.measurement_limitations > 0);
2796    }
2797    files.sort_by(|left, right| {
2798        gap_metric_value(right, metric)
2799            .cmp(&gap_metric_value(left, metric))
2800            .then_with(|| {
2801                right
2802                    .measurement_limitations
2803                    .cmp(&left.measurement_limitations)
2804            })
2805            .then_with(|| left.file.cmp(&right.file))
2806    });
2807    let total = files.len();
2808    let page = files
2809        .into_iter()
2810        .skip(offset)
2811        .take(limit)
2812        .collect::<Vec<_>>();
2813    let page_info = pagination(offset, limit, page.len(), total);
2814    let filters = CoverageQueryFilters {
2815        outcome: match view {
2816            CoverageViewId::All => "all",
2817            CoverageViewId::Passed => "passed",
2818            CoverageViewId::Failed => "failed",
2819        }
2820        .into(),
2821        kind: kind.map(str::to_owned),
2822        runner: runner.map(str::to_owned),
2823    };
2824    let data = if gaps_only {
2825        CoverageFileQueryData::Gaps(CoverageGapsData {
2826            run: run.into(),
2827            filters,
2828            metric,
2829            gaps: page,
2830        })
2831    } else {
2832        CoverageFileQueryData::Files(CoverageFilesData {
2833            run: run.into(),
2834            filters,
2835            metric,
2836            files: page,
2837        })
2838    };
2839    Ok(CoverageFileQueryResult {
2840        data,
2841        pagination: page_info,
2842    })
2843}
2844
2845#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2846enum ObligationMetric {
2847    Lines,
2848    Statements,
2849    Functions,
2850    Branches,
2851    Mcdc,
2852}
2853
2854impl ObligationMetric {
2855    fn selected(self, metric: MinimizeMetric) -> bool {
2856        metric == MinimizeMetric::All
2857            || matches!(
2858                (self, metric),
2859                (Self::Lines, MinimizeMetric::Lines)
2860                    | (Self::Statements, MinimizeMetric::Statements)
2861                    | (Self::Functions, MinimizeMetric::Functions)
2862                    | (Self::Branches, MinimizeMetric::Branches)
2863                    | (Self::Mcdc, MinimizeMetric::Mcdc)
2864            )
2865    }
2866
2867    fn public(self) -> MinimizeMetric {
2868        match self {
2869            Self::Lines => MinimizeMetric::Lines,
2870            Self::Statements => MinimizeMetric::Statements,
2871            Self::Functions => MinimizeMetric::Functions,
2872            Self::Branches => MinimizeMetric::Branches,
2873            Self::Mcdc => MinimizeMetric::Mcdc,
2874        }
2875    }
2876}
2877
2878#[derive(Clone)]
2879struct Obligation {
2880    id: String,
2881    metric: ObligationMetric,
2882    /// Any one complete option satisfies this obligation.
2883    options: Vec<Vec<String>>,
2884}
2885
2886struct ObligationModel {
2887    obligations: Vec<Obligation>,
2888    setup_by_file: BTreeMap<String, Vec<String>>,
2889    tests_by_file: BTreeMap<String, Vec<String>>,
2890    background: Vec<String>,
2891}
2892
2893impl ObligationModel {
2894    fn expand(&self, selected: &BTreeSet<String>) -> BTreeSet<String> {
2895        let mut expanded = selected.clone();
2896        expanded.extend(self.background.iter().cloned());
2897        for (file, setup_ids) in &self.setup_by_file {
2898            if self
2899                .tests_by_file
2900                .get(file)
2901                .is_some_and(|tests| tests.iter().any(|id| selected.contains(id)))
2902            {
2903                expanded.extend(setup_ids.iter().cloned());
2904            }
2905        }
2906        expanded
2907    }
2908}
2909
2910fn deduplicate_options(options: impl IntoIterator<Item = Vec<String>>) -> Vec<Vec<String>> {
2911    let mut unique = BTreeMap::<String, Vec<String>>::new();
2912    for mut option in options {
2913        option.sort();
2914        option.dedup();
2915        let key = option.join("\0");
2916        unique.entry(key).or_insert(option);
2917    }
2918    unique.into_values().collect()
2919}
2920
2921fn evidence_choices(
2922    ids: &[Id],
2923    tests: &HashMap<&str, &crate::coverage_report::TestCoverageResult>,
2924    candidates: &BTreeSet<String>,
2925    tests_by_file: &BTreeMap<String, Vec<String>>,
2926) -> Vec<Vec<String>> {
2927    let mut choices = Vec::new();
2928    for id in ids {
2929        let Some(test) = tests.get(id.as_str()) else {
2930            continue;
2931        };
2932        if test.role == "background" {
2933            choices.push(Vec::new());
2934        } else if test.role == "setup" {
2935            if let Some(file) = &test.file {
2936                choices.extend(
2937                    tests_by_file
2938                        .get(file)
2939                        .into_iter()
2940                        .flatten()
2941                        .map(|candidate| vec![candidate.clone()]),
2942                );
2943            }
2944        } else if candidates.contains(id.as_str()) {
2945            choices.push(vec![id.to_string()]);
2946        }
2947    }
2948    deduplicate_options(choices)
2949}
2950
2951fn build_obligations(view: &CoverageView, candidates: &BTreeSet<String>) -> ObligationModel {
2952    let tests = view
2953        .tests
2954        .iter()
2955        .map(|test| (test.id.as_str(), test))
2956        .collect::<HashMap<_, _>>();
2957    let mut tests_by_file = BTreeMap::<String, Vec<String>>::new();
2958    let mut setup_by_file = BTreeMap::<String, Vec<String>>::new();
2959    let mut background = Vec::new();
2960    for test in &view.tests {
2961        match test.role.as_str() {
2962            "test" if candidates.contains(&test.id) => {
2963                if let Some(file) = &test.file {
2964                    tests_by_file
2965                        .entry(file.clone())
2966                        .or_default()
2967                        .push(test.id.clone());
2968                }
2969            }
2970            "setup" => {
2971                if let Some(file) = &test.file {
2972                    setup_by_file
2973                        .entry(file.clone())
2974                        .or_default()
2975                        .push(test.id.clone());
2976                }
2977            }
2978            "background" => background.push(test.id.clone()),
2979            _ => {}
2980        }
2981    }
2982    let choices = |ids: &[Id]| evidence_choices(ids, &tests, candidates, &tests_by_file);
2983    let mut obligations = Vec::new();
2984    let mut unique_lines = BTreeMap::new();
2985    for line in &view.lines {
2986        unique_lines.insert((line.file.as_str(), line.line), line);
2987    }
2988    for ((file, line), result) in unique_lines {
2989        obligations.push(Obligation {
2990            id: format!("line:{file}:{line}"),
2991            metric: ObligationMetric::Lines,
2992            options: choices(&result.tests),
2993        });
2994    }
2995    for point in &view.points {
2996        let (kind, metric) = match point.meta.kind {
2997            crate::coverage_analysis::PointKind::Statement => {
2998                ("statement", ObligationMetric::Statements)
2999            }
3000            crate::coverage_analysis::PointKind::Function => {
3001                ("function", ObligationMetric::Functions)
3002            }
3003        };
3004        obligations.push(Obligation {
3005            id: format!("{kind}:{}", point.meta.id),
3006            metric,
3007            options: choices(&point.tests),
3008        });
3009    }
3010    for branch in &view.branches {
3011        for alternative in &branch.alternatives {
3012            obligations.push(Obligation {
3013                id: format!("branch:{}:{}", branch.meta.id, alternative.id),
3014                metric: ObligationMetric::Branches,
3015                options: choices(&alternative.tests),
3016            });
3017        }
3018    }
3019    for decision in &view.decisions {
3020        for condition in 0..decision.meta.conditions.len() {
3021            let mut options = Vec::new();
3022            for left in 0..decision.vector_observations.len() {
3023                for right in (left + 1)..decision.vector_observations.len() {
3024                    let first = &decision.vector_observations[left];
3025                    let second = &decision.vector_observations[right];
3026                    if !is_independence_pair(&first.vector, &second.vector, condition) {
3027                        continue;
3028                    }
3029                    for first_choice in choices(&first.tests) {
3030                        for second_choice in choices(&second.tests) {
3031                            let mut combined = first_choice.clone();
3032                            combined.extend(second_choice);
3033                            options.push(combined);
3034                        }
3035                    }
3036                }
3037            }
3038            obligations.push(Obligation {
3039                id: format!("mcdc:{}:{condition}", decision.meta.id),
3040                metric: ObligationMetric::Mcdc,
3041                options: deduplicate_options(options),
3042            });
3043        }
3044    }
3045    ObligationModel {
3046        obligations,
3047        setup_by_file,
3048        tests_by_file,
3049        background,
3050    }
3051}
3052
3053fn percentage(metric: ObligationMetric, summary: &CoverageSummary) -> f64 {
3054    match metric {
3055        ObligationMetric::Lines => summary.lines.percentage,
3056        ObligationMetric::Statements => summary.statements.percentage,
3057        ObligationMetric::Functions => summary.functions.percentage,
3058        ObligationMetric::Branches => summary.branches.percentage,
3059        ObligationMetric::Mcdc => summary.condition_coverage_pct,
3060    }
3061}
3062
3063fn obligation_satisfied(obligation: &Obligation, selected: &BTreeSet<String>) -> bool {
3064    obligation
3065        .options
3066        .iter()
3067        .any(|option| option.iter().all(|test| selected.contains(test)))
3068}
3069
3070struct Search<'a> {
3071    obligations: &'a [Obligation],
3072    skip_limits: BTreeMap<ObligationMetric, usize>,
3073    best: BTreeSet<String>,
3074    explored_states: usize,
3075    max_states: usize,
3076    seen: BTreeSet<String>,
3077    candidate_tests: usize,
3078    target: f64,
3079    metric: MinimizeMetric,
3080}
3081
3082impl Search<'_> {
3083    fn visit(
3084        &mut self,
3085        selected: BTreeSet<String>,
3086        skipped: BTreeSet<String>,
3087        skipped_by_metric: BTreeMap<ObligationMetric, usize>,
3088    ) -> Result<(), QueryError> {
3089        self.explored_states += 1;
3090        if self.explored_states > self.max_states {
3091            return Err(QueryError::ComplexityLimit {
3092                candidate_tests: self.candidate_tests,
3093                obligations: self.obligations.len(),
3094                explored_states: self.explored_states,
3095                max_states: self.max_states,
3096                target: self.target,
3097                metric: self.metric,
3098            });
3099        }
3100        if selected.len() >= self.best.len() {
3101            return Ok(());
3102        }
3103        let state_key = format!(
3104            "{}|{}",
3105            selected.iter().cloned().collect::<Vec<_>>().join(","),
3106            skipped.iter().cloned().collect::<Vec<_>>().join(",")
3107        );
3108        if !self.seen.insert(state_key) {
3109            return Ok(());
3110        }
3111        let mut unmet = self
3112            .obligations
3113            .iter()
3114            .filter(|obligation| {
3115                !skipped.contains(&obligation.id) && !obligation_satisfied(obligation, &selected)
3116            })
3117            .collect::<Vec<_>>();
3118        if unmet.is_empty() {
3119            self.best = selected;
3120            return Ok(());
3121        }
3122        unmet.sort_by(|left, right| {
3123            let feasible = |obligation: &Obligation| {
3124                obligation
3125                    .options
3126                    .iter()
3127                    .filter(|option| option.iter().any(|test| !selected.contains(test)))
3128                    .count()
3129            };
3130            feasible(left)
3131                .cmp(&feasible(right))
3132                .then_with(|| left.id.cmp(&right.id))
3133        });
3134        let obligation = unmet[0];
3135        let mut additions = deduplicate_options(obligation.options.iter().filter_map(|option| {
3136            let addition = option
3137                .iter()
3138                .filter(|test| !selected.contains(*test))
3139                .cloned()
3140                .collect::<Vec<_>>();
3141            (!addition.is_empty()).then_some(addition)
3142        }));
3143        additions.sort_by(|left, right| {
3144            left.len()
3145                .cmp(&right.len())
3146                .then_with(|| left.join("\0").cmp(&right.join("\0")))
3147        });
3148        for addition in additions {
3149            if selected.len() + addition.len() >= self.best.len() {
3150                continue;
3151            }
3152            let mut next = selected.clone();
3153            next.extend(addition);
3154            self.visit(next, skipped.clone(), skipped_by_metric.clone())?;
3155        }
3156        let skipped_count = skipped_by_metric
3157            .get(&obligation.metric)
3158            .copied()
3159            .unwrap_or(0);
3160        if skipped_count
3161            < self
3162                .skip_limits
3163                .get(&obligation.metric)
3164                .copied()
3165                .unwrap_or(0)
3166        {
3167            let mut next_skipped = skipped;
3168            next_skipped.insert(obligation.id.clone());
3169            let mut next_counts = skipped_by_metric;
3170            next_counts.insert(obligation.metric, skipped_count + 1);
3171            self.visit(selected, next_skipped, next_counts)?;
3172        }
3173        Ok(())
3174    }
3175}
3176
3177pub fn minimum_test_set(
3178    view: &CoverageView,
3179    target: f64,
3180    metric: MinimizeMetric,
3181    max_states: usize,
3182) -> Result<MinimumTestSetResult, QueryError> {
3183    use crate::coverage_report::coverage_is_complete;
3184    if !target.is_finite() || !(0.0..=100.0).contains(&target) {
3185        return Err(QueryError::InvalidTarget(target));
3186    }
3187    if view.tests.iter().any(|test| {
3188        test.role == "background" && (!test.hits.is_empty() || !test.decisions.is_empty())
3189    }) {
3190        return Err(QueryError::UnattributedEvidence);
3191    }
3192    // And a test whose own coverage nothing recorded cannot be minimized
3193    // around either. Its empty hit set would read as a test that adds nothing
3194    // to any set, so the smallest set would drop it -- and what it covers is
3195    // precisely what nothing knows. The check above catches this whenever the
3196    // run-wide record holds anything, which is nearly always; this catches the
3197    // case where those tests happened to reach nothing measured.
3198    if view
3199        .tests
3200        .iter()
3201        .any(|test| test.role == "test" && !coverage_is_complete(&test.attribution))
3202    {
3203        return Err(QueryError::UnattributedEvidence);
3204    }
3205    let candidate_tests = view
3206        .tests
3207        .iter()
3208        .filter(|test| test.role == "test")
3209        .map(|test| test.id.clone())
3210        .collect::<BTreeSet<_>>();
3211    let model = build_obligations(view, &candidate_tests);
3212    let obligations = model
3213        .obligations
3214        .iter()
3215        .filter(|obligation| obligation.metric.selected(metric))
3216        .cloned()
3217        .collect::<Vec<_>>();
3218    let mut totals = BTreeMap::<ObligationMetric, usize>::new();
3219    for obligation in &obligations {
3220        *totals.entry(obligation.metric).or_default() += 1;
3221    }
3222    let metrics = [
3223        ObligationMetric::Lines,
3224        ObligationMetric::Statements,
3225        ObligationMetric::Functions,
3226        ObligationMetric::Branches,
3227        ObligationMetric::Mcdc,
3228    ]
3229    .into_iter()
3230    .filter(|candidate| candidate.selected(metric))
3231    .collect::<Vec<_>>();
3232    let skip_limits = metrics
3233        .iter()
3234        .map(|selected_metric| {
3235            let total = totals.get(selected_metric).copied().unwrap_or(0);
3236            let required = ((total as f64 * target) / 100.0).ceil() as usize;
3237            (*selected_metric, total.saturating_sub(required))
3238        })
3239        .collect::<BTreeMap<_, _>>();
3240    let full_expanded = model.expand(&candidate_tests);
3241    let full_summary = coverage_summary_for_tests(view, &full_expanded)?;
3242    for selected_metric in &metrics {
3243        let reachable = percentage(*selected_metric, &full_summary);
3244        if reachable + 1e-9 < target {
3245            return Err(QueryError::TargetUnreachable {
3246                metric: selected_metric.public(),
3247                target,
3248                reachable,
3249            });
3250        }
3251    }
3252    let mut search = Search {
3253        obligations: &obligations,
3254        skip_limits,
3255        best: candidate_tests.clone(),
3256        explored_states: 0,
3257        max_states,
3258        seen: BTreeSet::new(),
3259        candidate_tests: candidate_tests.len(),
3260        target,
3261        metric,
3262    };
3263    search.visit(BTreeSet::new(), BTreeSet::new(), BTreeMap::new())?;
3264    let expanded = model.expand(&search.best);
3265    let summary = coverage_summary_for_tests(view, &expanded)?;
3266    Ok(MinimumTestSetResult {
3267        optimal: true,
3268        target,
3269        metric,
3270        selected: search.best.into_iter().collect(),
3271        expanded: expanded.into_iter().collect(),
3272        summary,
3273        explored_states: search.explored_states,
3274    })
3275}
3276
3277#[cfg(test)]
3278mod tests {
3279    use crate::{
3280        coverage_analysis::McdcVector,
3281        coverage_report::{
3282            CoverageManifest, CoverageReport, CoverageReportRequest, DecisionMeta, ExitCodeInput,
3283            RawTestResult, RuntimeSnapshot, TestProvenance, analyze_coverage_results,
3284        },
3285    };
3286
3287    use super::*;
3288
3289    #[test]
3290    fn all_metric_keeps_statement_only_files_in_the_gap_set() {
3291        let gap = IndexedFileGap {
3292            view: CoverageViewId::All,
3293            file: "src/statement.js".into(),
3294            uncovered_lines: 0,
3295            uncovered_statements: 1,
3296            uncovered_functions: 0,
3297            missing_branches: 0,
3298            missing_mcdc_conditions: 0,
3299            measurement_limitations: 0,
3300            limitation_kinds: Vec::new(),
3301            covered_by_other_tests: crate::coverage_index::IndexedGapDimensions {
3302                lines: 0,
3303                statements: 0,
3304                functions: 0,
3305                branches: 0,
3306                mcdc_conditions: 0,
3307            },
3308            uncovered_everywhere: crate::coverage_index::IndexedGapDimensions {
3309                lines: 0,
3310                statements: 1,
3311                functions: 0,
3312                branches: 0,
3313                mcdc_conditions: 0,
3314            },
3315            score: 0,
3316        };
3317        assert!(has_gap_for_metric(&gap, MinimizeMetric::All));
3318        assert!(has_gap_for_metric(&gap, MinimizeMetric::Statements));
3319        assert!(!has_gap_for_metric(&gap, MinimizeMetric::Lines));
3320    }
3321
3322    fn result(id: &str, vector: McdcVector) -> RawTestResult {
3323        RawTestResult {
3324            test_id: Some(id.into()),
3325            scope: None,
3326            test: id.into(),
3327            test_file: Some("tests/permission.test.js".into()),
3328            title: None,
3329            retry: Some(0),
3330            status: Some("passed".into()),
3331            expected_status: None,
3332            flaky: false,
3333            provenance: TestProvenance {
3334                runner: "node:test".into(),
3335                kind: "unit".into(),
3336                project: None,
3337                source: "runner-default".into(),
3338            },
3339            role: "test".into(),
3340            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
3341            phases: Vec::new(),
3342            runtime: vec![RuntimeSnapshot {
3343                decisions: vec![crate::coverage_report::DecisionSnapshot {
3344                    meta: decision(),
3345                    vectors: vec![vector],
3346                }],
3347                hits: Vec::new(),
3348                events: Vec::new(),
3349                logicals: Vec::new(),
3350                phase_id: None,
3351            }],
3352            browser: Vec::new(),
3353            server: Vec::new(),
3354        }
3355    }
3356
3357    fn decision() -> DecisionMeta {
3358        DecisionMeta {
3359            id: "decision".into(),
3360            file: "src/permission.js".into(),
3361            line: 1,
3362            column: 1,
3363            source: "admin || owner".into(),
3364            conditions: vec!["admin".into(), "owner".into()],
3365            kind: "if".into(),
3366        }
3367    }
3368
3369    fn report(mut results: Vec<RawTestResult>) -> CoverageReport {
3370        analyze_coverage_results(&CoverageReportRequest {
3371            run_id: "run".into(),
3372            manifest: CoverageManifest {
3373                unmeasured: Vec::new(),
3374                decisions: vec![decision()],
3375                points: Vec::new(),
3376                branches: Vec::new(),
3377                limitations: Vec::new(),
3378                scope: None,
3379            },
3380            raw_results: std::mem::take(&mut results),
3381            generated_at: "time".into(),
3382            coverage_model: None,
3383            integrity: None,
3384            test_exit_code: ExitCodeInput::Present(Some(0)),
3385        })
3386        .unwrap()
3387    }
3388
3389    #[test]
3390    fn recomputes_mcdc_witnesses_and_removes_a_redundant_vector() {
3391        let report = report(vec![
3392            result(
3393                "admin",
3394                McdcVector {
3395                    values: vec![Some(true), None],
3396                    outcome: true,
3397                },
3398            ),
3399            result(
3400                "owner",
3401                McdcVector {
3402                    values: vec![Some(false), Some(true)],
3403                    outcome: true,
3404                },
3405            ),
3406            result(
3407                "both",
3408                McdcVector {
3409                    values: vec![Some(true), None],
3410                    outcome: true,
3411                },
3412            ),
3413            result(
3414                "neither",
3415                McdcVector {
3416                    values: vec![Some(false), Some(false)],
3417                    outcome: false,
3418                },
3419            ),
3420        ]);
3421        let minimized = minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 5_000).unwrap();
3422        assert_eq!(minimized.selected.len(), 3);
3423        assert!(minimized.selected.contains(&"owner".into()));
3424        assert!(minimized.selected.contains(&"neither".into()));
3425        assert_eq!(minimized.summary.condition_coverage_pct, 100.0);
3426    }
3427
3428    #[test]
3429    fn a_test_that_belongs_to_no_test_is_not_minimized_away() {
3430        // The first guard catches this whenever the run-wide record holds
3431        // anything, which is nearly always -- so it is the rare run, where
3432        // those tests happened to reach nothing measured, that reaches this
3433        // one. Without it the smallest set would quietly drop a test whose
3434        // coverage is unknown rather than absent, and report the drop as a
3435        // saving.
3436        let mut parallel = result(
3437            "parallel",
3438            McdcVector {
3439                values: vec![Some(false), Some(false)],
3440                outcome: false,
3441            },
3442        );
3443        parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
3444        assert!(matches!(
3445            minimum_test_set(
3446                &report(vec![parallel]).view,
3447                100.0,
3448                MinimizeMetric::Mcdc,
3449                5_000,
3450            ),
3451            Err(QueryError::UnattributedEvidence)
3452        ));
3453    }
3454
3455    #[test]
3456    fn the_summary_counts_each_test_under_the_attribution_it_carries() {
3457        // These three numbers are what the `Attribution` line reads out, and
3458        // nothing -- here or in the integration scripts -- had ever checked
3459        // that a test lands in the bucket it was written with.
3460        let vector = || McdcVector {
3461            values: vec![Some(false), Some(false)],
3462            outcome: false,
3463        };
3464        let mut serial = result("serial", vector());
3465        serial.attribution = crate::coverage_report::ATTRIBUTION_EXACT.into();
3466        let mut parallel = result("parallel", vector());
3467        parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
3468        let mut ruby = result("ruby", vector());
3469        ruby.attribution = crate::coverage_report::ATTRIBUTION_PARTIAL.into();
3470        let mut also_parallel = result("also-parallel", vector());
3471        also_parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
3472        let report = report(vec![serial, parallel, ruby, also_parallel]);
3473
3474        let nonce = std::time::SystemTime::now()
3475            .duration_since(std::time::UNIX_EPOCH)
3476            .unwrap()
3477            .as_nanos();
3478        let root = std::env::temp_dir().join(format!(
3479            "supercov-attribution-counts-{}-{nonce}",
3480            std::process::id()
3481        ));
3482        std::fs::create_dir(&root).unwrap();
3483        let path = root.join("query-index.v1.bin");
3484        let identity = crate::query_index::QueryIndexIdentity {
3485            evidence_sha256: [1; 32],
3486            evidence_bytes: 100,
3487            analysis_sha256: [2; 32],
3488            producer_sha256: [3; 32],
3489            archive_schema_version: 2,
3490        };
3491        crate::query_index::write_query_index(
3492            &crate::coverage_index::coverage_index_sections(&report).unwrap(),
3493            &identity,
3494            &path,
3495        )
3496        .unwrap();
3497        let container = crate::query_index::QueryIndex::open(&path, &identity).unwrap();
3498        let index = crate::coverage_index::CoverageIndex::new(&container).unwrap();
3499
3500        let summary = coverage_summary_query(
3501            &index,
3502            CoverageSummaryQueryOptions {
3503                run: "run",
3504                view: CoverageViewId::All,
3505                kind: None,
3506                runner: None,
3507                valid: true,
3508                test_exit_code: Some(0),
3509                stale: false,
3510                stale_reasons: Vec::new(),
3511            },
3512        )
3513        .unwrap();
3514        assert_eq!(summary.test_attribution.exact, 1);
3515        assert_eq!(summary.test_attribution.run_wide, 2);
3516        assert_eq!(summary.test_attribution.partial, 1);
3517        assert_eq!(summary.test_attribution.total(), 4);
3518        std::fs::remove_dir_all(&root).ok();
3519    }
3520
3521    #[test]
3522    fn refuses_background_evidence() {
3523        let mut aggregate = result(
3524            "aggregate",
3525            McdcVector {
3526                values: vec![Some(false), Some(false)],
3527                outcome: false,
3528            },
3529        );
3530        aggregate.role = "background".into();
3531        assert!(matches!(
3532            minimum_test_set(
3533                &report(vec![aggregate]).view,
3534                100.0,
3535                MinimizeMetric::Mcdc,
3536                5_000,
3537            ),
3538            Err(QueryError::UnattributedEvidence)
3539        ));
3540    }
3541
3542    #[test]
3543    fn bounds_the_exact_search() {
3544        let report = report(vec![
3545            result(
3546                "admin",
3547                McdcVector {
3548                    values: vec![Some(true), None],
3549                    outcome: true,
3550                },
3551            ),
3552            result(
3553                "owner",
3554                McdcVector {
3555                    values: vec![Some(false), Some(true)],
3556                    outcome: true,
3557                },
3558            ),
3559            result(
3560                "neither",
3561                McdcVector {
3562                    values: vec![Some(false), Some(false)],
3563                    outcome: false,
3564                },
3565            ),
3566        ]);
3567        assert!(matches!(
3568            minimum_test_set(&report.view, 100.0, MinimizeMetric::Mcdc, 1),
3569            Err(QueryError::ComplexityLimit { .. })
3570        ));
3571    }
3572}