Skip to main content

backhopper_core/model/
batch.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//! Public wire-format types for `check batch`. Promoted from the
6//! previously-private types in `backhopper-cli::commands::check`.
7
8use std::collections::BTreeSet;
9use std::num::NonZeroU32;
10
11use serde::{Deserialize, Serialize};
12
13use crate::model::apply::ApplyForecast;
14use crate::model::clearance::RoundClearance;
15use crate::model::findings::TargetFindings;
16use crate::model::fingerprint::VerdictFingerprint;
17use crate::model::names::{CommitSha, ProjectName, RelativePath, SeriesName, TagName};
18use crate::model::pin::Pin;
19use crate::model::pr_commit::PrCommit;
20use crate::model::producer::Producer;
21use crate::model::resolver_coverage::ResolverCoverage;
22use crate::model::verdict::{Diagnostics, PatchFacts, SeriesVerdict};
23
24/// Top-level payload of a `check batch` envelope.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
27pub struct BatchPayload {
28    pub queried_against: Vec<BatchQuery>,
29    pub results: Vec<BatchResult>,
30
31    /// Projects evaluated as the backport subject itself, excluded from
32    /// the tracked-dependency tally. `None` means the producing binary
33    /// predates the field, not "no self project": a current producer
34    /// always emits `Some`, even when empty, so it is never skipped.
35    #[serde(default)]
36    pub self_projects: Option<BTreeSet<ProjectName>>,
37
38    /// Symbol classes this producer checks. A corpus row keeps it so the
39    /// fold routes bug-vs-gap against the producing binary, not the one
40    /// running the fold. `None` for a producer that predates the field.
41    #[serde(default)]
42    pub resolver_coverage: Option<ResolverCoverage>,
43
44    /// The fingerprint generation this producer stamped. Lets a corpus
45    /// spanning a bump segregate generations instead of reading an
46    /// unexplained zero-match. `None` for a producer that predates it.
47    #[serde(default)]
48    pub fingerprint_version: Option<u32>,
49}
50
51impl BatchPayload {
52    /// Round-level clearance over these rows, excluding `self_projects`
53    /// from the tracked-dependency tally.
54    #[must_use]
55    pub fn clearance(&self, self_projects: &BTreeSet<ProjectName>) -> RoundClearance {
56        RoundClearance::from_results(&self.results, self_projects)
57    }
58
59    /// The producer generation that wrote `self_projects`,
60    /// `resolver_coverage`, and `fingerprint_version`.
61    #[must_use]
62    pub fn producer(&self) -> Producer<'_> {
63        Producer::from_fields(
64            self.self_projects.as_ref(),
65            self.resolver_coverage.as_ref(),
66            self.fingerprint_version,
67        )
68    }
69
70    /// Clearance from the self-projects the producer recorded on the
71    /// wire. `None` when the producer predates the field; the caller
72    /// then passes its own set to `clearance`.
73    #[must_use]
74    pub fn clearance_self_inferred(&self) -> Option<RoundClearance> {
75        match self.producer() {
76            Producer::Current { self_projects, .. } => Some(self.clearance(self_projects)),
77            Producer::BeforeV12 => None,
78        }
79    }
80}
81
82/// One series queried by the batch. Holds the pin set used during
83/// evaluation so consumers can correlate per-row results.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
86pub struct BatchQuery {
87    pub series: SeriesName,
88    pub pins: Vec<PinPayload>,
89}
90
91/// On-wire pin shape used inside `BatchQuery`. Distinct from
92/// `crate::model::pin::Pin` because the wire form is a `(project, tag)`
93/// projection without the resolution logic.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
96pub struct PinPayload {
97    pub project: ProjectName,
98    pub tag: TagName,
99}
100
101impl From<&Pin> for PinPayload {
102    fn from(pin: &Pin) -> Self {
103        Self {
104            project: pin.project.clone(),
105            tag: pin.tag.clone(),
106        }
107    }
108}
109
110/// One row in the batch result set.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub struct BatchResult {
114    pub commit: CommitSha,
115    pub series: SeriesName,
116    pub verdict: SeriesVerdict,
117
118    #[serde(default, skip_serializing_if = "Diagnostics::is_empty")]
119    pub diagnostics: Diagnostics,
120
121    #[serde(default, skip_serializing_if = "PatchFacts::is_empty")]
122    pub patch_facts: PatchFacts,
123
124    /// Paths the analysed patch touched, in diff-encounter order.
125    /// Always emitted; an empty vec means the patch touched zero
126    /// files.
127    #[serde(default)]
128    pub touched_paths: Vec<RelativePath>,
129
130    /// Inner PR-branch commits for 2-parent merge SHAs. `None` for
131    /// non-2-parent merges and non-merge SHAs.
132    ///
133    /// Do NOT add `skip_serializing_if = "Option::is_none"`. The
134    /// `None` vs `Some(vec![])` distinction is part of the wire contract.
135    #[serde(default)]
136    pub pr_commits: Option<Vec<PrCommit>>,
137
138    /// Number of parents of the evaluated commit: 1 for plain
139    /// commits, 2 for PR merges, 3+ for octopus merges. `None` only
140    /// when the envelope was produced by a pre-v7 binary; v7
141    /// producers always emit it.
142    #[serde(default)]
143    pub parent_count: Option<NonZeroU32>,
144
145    /// Stable join key tying this row's verdict to a later build
146    /// outcome. Absent for uncacheable rows (`--no-cache`, an
147    /// unresolvable pin) and pre-fingerprint producers.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub verdict_fingerprint: Option<VerdictFingerprint>,
150
151    /// Apply-axis prediction for this row. `None` means no target
152    /// context was supplied, so the axis was not evaluated: never
153    /// "clean". A producer with a target context always emits `Some`,
154    /// even when the patch touched zero files.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub apply: Option<ApplyForecast>,
157
158    /// Symbol-axis target findings for this row. `None` means no
159    /// target context was supplied, so the axis was not evaluated:
160    /// never "clean". A producer with a target context always emits
161    /// `Some`, even when no check produced a finding.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub target_findings: Option<TargetFindings>,
164}