Skip to main content

backhopper_core/model/
findings.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Row-level symbol-axis findings resolved against the target tree.
6//! The same reasons also merge into applicable pin verdicts; this
7//! record is the copy that survives inapplicable ones, so a suite-only
8//! pick cannot silently drop what the target checks found.
9
10use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use crate::model::resolver_coverage::ResolverClass;
15use crate::model::verdict::Reason;
16
17/// Symbol-axis findings for one row, resolved against the target tree.
18/// Present exactly when a target context was supplied; independent of
19/// every pin verdict, so an inapplicable row still carries its
20/// findings.
21#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23pub struct TargetFindings {
24    /// Every reason the live-axis target checks produced for this row,
25    /// in production order.
26    pub reasons: Vec<Reason>,
27}
28
29impl TargetFindings {
30    #[must_use]
31    pub fn is_empty(&self) -> bool {
32        self.reasons.is_empty()
33    }
34
35    /// Finding counts by resolver class, for ranking.
36    #[must_use]
37    pub fn by_class(&self) -> Vec<(ResolverClass, usize)> {
38        let mut counts: BTreeMap<ResolverClass, usize> = BTreeMap::new();
39        for reason in &self.reasons {
40            if let Some(class) = reason.resolver_class() {
41                *counts.entry(class).or_insert(0) += 1;
42            }
43        }
44        counts.into_iter().collect()
45    }
46
47    /// Findings with no resolver class, so the class view never hides
48    /// a reason.
49    #[must_use]
50    pub fn unclassified(&self) -> usize {
51        self.reasons
52            .iter()
53            .filter(|r| r.resolver_class().is_none())
54            .count()
55    }
56
57    #[must_use]
58    pub fn blocking(&self) -> bool {
59        self.reasons.iter().any(Reason::is_blocking)
60    }
61}