Skip to main content

backhopper_core/model/
check_payload.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//! The canonical `check`-family wire payload. One definition the CLI
6//! serializes, the driver parses, and `schema show` documents, so the
7//! three cannot drift apart.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use serde::{Deserialize, Serialize};
12
13use crate::model::apply::ApplyForecast;
14use crate::model::batch::PinPayload;
15use crate::model::evaluation::{
16    AggregateVerdict, BehaviourModuleMissingFinding, HeaderFileMissingFinding,
17    SeriesEvaluationView, TestModuleSymbolMissingFinding, VersionedMachineSnapshotMissingFinding,
18    WireConstantBindingsMissingFinding,
19};
20use crate::model::findings::TargetFindings;
21use crate::model::fingerprint::VerdictFingerprint;
22use crate::model::names::{ModuleName, ProjectName, RelativePath, SeriesName, TagName};
23use crate::model::pin::Pin;
24use crate::model::pr_commit::PrCommit;
25use crate::model::producer::Producer;
26use crate::model::resolver_coverage::ResolverCoverage;
27use crate::model::summary::VerdictKind;
28use crate::model::verdict::{Diagnostics, PinVerdict, Reason, SeriesVerdict};
29
30/// A project the untracked-call analysis suggests adding to config so its
31/// calls become tracked-dependency evidence.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34pub struct ProjectSuggestion {
35    pub name: String,
36    pub modules: Vec<String>,
37    pub call_sites: usize,
38}
39
40/// What a `check`-family verb was queried against: a single pin or a
41/// whole series.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
44#[serde(tag = "kind", rename_all = "snake_case")]
45pub enum QueriedAgainst {
46    /// `check ... --project NAME --tag TAG`.
47    Pin { project: ProjectName, tag: TagName },
48    /// `check ... --series NAME`.
49    Series {
50        name: SeriesName,
51        pins: Vec<PinPayload>,
52    },
53}
54
55/// The parsed payload of every `check`-family verb.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58pub struct CheckPayload {
59    /// What the verb was queried against (a series or a pin).
60    pub queried_against: QueriedAgainst,
61    /// Per-pin verdicts plus summary.
62    pub results: SeriesVerdict,
63    /// Series-wide diagnostics (untracked calls, suggested suites).
64    #[serde(default, skip_serializing_if = "Diagnostics::is_empty")]
65    pub diagnostics: Diagnostics,
66    /// Projects the untracked-call analysis suggests adding.
67    #[serde(default, skip_serializing_if = "Vec::is_empty")]
68    pub project_suggestions: Vec<ProjectSuggestion>,
69    /// Inner PR-branch commits for 2-parent merge inputs, `None` for
70    /// everything else. The `None` vs `Some(vec![])` distinction is
71    /// part of the wire contract, so no `skip_serializing_if`.
72    #[serde(default)]
73    pub pr_commits: Option<Vec<PrCommit>>,
74    /// Projects excluded from the tracked-dependency tally. Always
75    /// emitted so a consumer can tell an empty set from an old binary.
76    #[serde(default)]
77    pub self_projects: Option<BTreeSet<ProjectName>>,
78    /// Stable join key for this verdict, omitted for inputs with no
79    /// cache key (patch, range, PR, or an unresolvable pin).
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub verdict_fingerprint: Option<VerdictFingerprint>,
82    /// Symbol classes this binary checks, for the measurement fold.
83    /// Always emitted so a consumer tells it from an old binary.
84    #[serde(default)]
85    pub resolver_coverage: Option<ResolverCoverage>,
86    /// The fingerprint generation this binary stamped. Always emitted.
87    #[serde(default)]
88    pub fingerprint_version: Option<u32>,
89    /// Apply-axis prediction. Absent when no target context was
90    /// supplied: the axis was not evaluated, never "clean".
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub apply: Option<ApplyForecast>,
93    /// Symbol-axis target findings, `None` when no target context.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub target_findings: Option<TargetFindings>,
96}
97
98impl CheckPayload {
99    /// A borrowed view exposing every query and finding accessor. The
100    /// same view is reachable from a batch row via
101    /// `BatchResult::evaluation`, so both check paths read alike.
102    #[must_use]
103    pub fn view(&self) -> SeriesEvaluationView<'_> {
104        SeriesEvaluationView::new(&self.results, &self.diagnostics)
105    }
106
107    /// The producer generation that wrote `self_projects`,
108    /// `resolver_coverage`, and `fingerprint_version`.
109    #[must_use]
110    pub fn producer(&self) -> Producer<'_> {
111        Producer::from_fields(
112            self.self_projects.as_ref(),
113            self.resolver_coverage.as_ref(),
114            self.fingerprint_version,
115        )
116    }
117
118    /// Self-excluded tracked-dependency count from the wire-recorded
119    /// self-projects. `None` when the producer predates the field.
120    #[must_use]
121    pub fn tracked_refs(&self) -> Option<u32> {
122        match self.producer() {
123            Producer::Current { self_projects, .. } => {
124                Some(self.view().tracked_refs(self_projects))
125            }
126            Producer::BeforeV12 => None,
127        }
128    }
129
130    /// See [`SeriesEvaluationView::worst_verdict`].
131    #[must_use]
132    pub fn worst_verdict(&self) -> AggregateVerdict {
133        self.view().worst_verdict()
134    }
135
136    /// See [`SeriesEvaluationView::has_blocking_reason`].
137    #[must_use]
138    pub fn has_blocking_reason(&self) -> bool {
139        self.view().has_blocking_reason()
140    }
141
142    /// See [`SeriesEvaluationView::pins_in`].
143    pub fn pins_in(&self, verdict: VerdictKind) -> impl Iterator<Item = &PinVerdict> {
144        self.view().pins_in(verdict)
145    }
146
147    /// See [`SeriesEvaluationView::reasons_for`].
148    pub fn reasons_for(&self, pin: &Pin) -> impl Iterator<Item = &Reason> {
149        self.view().reasons_for(pin)
150    }
151
152    /// See [`SeriesEvaluationView::pin_by_project`].
153    #[must_use]
154    pub fn pin_by_project(&self, project: &ProjectName) -> Option<&PinVerdict> {
155        self.view().pin_by_project(project)
156    }
157
158    /// See [`SeriesEvaluationView::missing_test_modules`].
159    #[must_use]
160    pub fn missing_test_modules(&self) -> &BTreeMap<RelativePath, BTreeMap<ModuleName, usize>> {
161        self.view().missing_test_modules()
162    }
163
164    /// See [`SeriesEvaluationView::test_module_symbol_missing`].
165    pub fn test_module_symbol_missing(
166        &self,
167    ) -> impl Iterator<Item = TestModuleSymbolMissingFinding<'_>> {
168        self.view().test_module_symbol_missing()
169    }
170
171    /// See [`SeriesEvaluationView::header_file_missing`].
172    pub fn header_file_missing(&self) -> impl Iterator<Item = HeaderFileMissingFinding<'_>> {
173        self.view().header_file_missing()
174    }
175
176    /// See [`SeriesEvaluationView::behaviour_module_missing`].
177    pub fn behaviour_module_missing(
178        &self,
179    ) -> impl Iterator<Item = BehaviourModuleMissingFinding<'_>> {
180        self.view().behaviour_module_missing()
181    }
182
183    /// See [`SeriesEvaluationView::versioned_machine_snapshot_missing`].
184    pub fn versioned_machine_snapshot_missing(
185        &self,
186    ) -> impl Iterator<Item = VersionedMachineSnapshotMissingFinding<'_>> {
187        self.view().versioned_machine_snapshot_missing()
188    }
189
190    /// See [`SeriesEvaluationView::wire_constant_bindings_missing`].
191    pub fn wire_constant_bindings_missing(
192        &self,
193    ) -> impl Iterator<Item = WireConstantBindingsMissingFinding<'_>> {
194        self.view().wire_constant_bindings_missing()
195    }
196}