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