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