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