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