Skip to main content

buildfix_types/
receipt.rs

1use camino::Utf8PathBuf;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5/// A generic sensor receipt envelope.
6///
7/// buildfix tries hard to be *tolerant* when reading receipts:
8/// - Unknown fields are ignored.
9/// - Optional fields may be absent.
10///
11/// The director and sensors should enforce stricter schema compliance; buildfix's job is to be useful
12/// with receipts "as found".
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReceiptEnvelope {
15    /// Schema identifier, e.g. "buildscan.report.v1".
16    pub schema: String,
17
18    pub tool: ToolInfo,
19
20    #[serde(default)]
21    pub run: RunInfo,
22
23    #[serde(default)]
24    pub verdict: Verdict,
25
26    #[serde(default)]
27    pub findings: Vec<Finding>,
28
29    /// Capabilities block for "No Green By Omission" pattern.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub capabilities: Option<ReceiptCapabilities>,
32
33    /// Optional, tool-specific payload.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub data: Option<serde_json::Value>,
36}
37
38/// Capabilities block describing what the sensor can/did check.
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct ReceiptCapabilities {
41    /// List of check_ids this sensor can emit.
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub check_ids: Vec<String>,
44
45    /// Scopes this sensor covers (e.g., "workspace", "crate").
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub scopes: Vec<String>,
48
49    /// True if some inputs could not be processed.
50    #[serde(default)]
51    pub partial: bool,
52
53    /// Reason for partial results, if applicable.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub reason: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ToolInfo {
60    pub name: String,
61
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub version: Option<String>,
64
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub repo: Option<String>,
67
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub commit: Option<String>,
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct RunInfo {
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub started_at: Option<DateTime<Utc>>,
76
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub ended_at: Option<DateTime<Utc>>,
79
80    /// Git HEAD SHA at the time this run was created.
81    /// Used to verify the plan is applied to the same repo state it was generated from.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub git_head_sha: Option<String>,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87pub struct Verdict {
88    #[serde(default)]
89    pub status: VerdictStatus,
90
91    #[serde(default)]
92    pub counts: Counts,
93
94    #[serde(default, skip_serializing_if = "Vec::is_empty")]
95    pub reasons: Vec<String>,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum VerdictStatus {
101    Pass,
102    Warn,
103    Fail,
104    #[default]
105    Unknown,
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize)]
109pub struct Counts {
110    #[serde(default)]
111    pub findings: u64,
112
113    #[serde(default)]
114    pub errors: u64,
115
116    #[serde(default)]
117    pub warnings: u64,
118}
119
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121pub struct Finding {
122    #[serde(default)]
123    pub severity: Severity,
124
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub check_id: Option<String>,
127
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub code: Option<String>,
130
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub message: Option<String>,
133
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub location: Option<Location>,
136
137    /// A stable key (ideally) for deduplication across runs.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub fingerprint: Option<String>,
140
141    /// Optional, tool-specific payload.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub data: Option<serde_json::Value>,
144
145    /// Confidence score (0.0 to 1.0) indicating certainty of the finding.
146    /// Higher values indicate more certainty.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub confidence: Option<f64>,
149
150    /// Provenance chain describing how the finding was derived.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub provenance: Option<Provenance>,
153
154    /// Context metadata for the finding.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub context: Option<FindingContext>,
157}
158
159impl Finding {
160    /// Returns true if this finding has high confidence (>= 0.9).
161    pub fn is_high_confidence(&self) -> bool {
162        self.confidence.is_some_and(|c| c >= 0.9)
163    }
164
165    /// Returns true if this finding has full consensus across all workspace crates.
166    pub fn has_full_consensus(&self) -> bool {
167        self.context
168            .as_ref()
169            .is_some_and(|ctx| ctx.workspace.as_ref().is_some_and(|ws| ws.all_crates_agree))
170    }
171
172    /// Returns true if multiple tools agree on this finding.
173    pub fn has_tool_agreement(&self) -> bool {
174        self.provenance.as_ref().is_some_and(|p| p.agreement)
175    }
176}
177
178/// Provenance information describing how a finding was derived.
179#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
180pub struct Provenance {
181    /// Method used to derive the finding (e.g., "dead_code_analysis", "license_detection")
182    pub method: String,
183
184    /// Tools/sensors that contributed to this finding
185    #[serde(default, skip_serializing_if = "Vec::is_empty")]
186    pub tools: Vec<String>,
187
188    /// Whether multiple tools agree on this finding
189    #[serde(default)]
190    pub agreement: bool,
191
192    /// Chain of evidence leading to this finding
193    #[serde(default, skip_serializing_if = "Vec::is_empty")]
194    pub evidence_chain: Vec<Evidence>,
195}
196
197/// A single piece of evidence in the provenance chain.
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct Evidence {
200    /// Source of the evidence: "repo", "lockfile", "registry", "workspace", "analysis"
201    pub source: String,
202
203    /// The value from this source
204    pub value: serde_json::Value,
205
206    /// Whether this evidence was validated
207    #[serde(default)]
208    pub validated: bool,
209}
210
211impl Default for Evidence {
212    fn default() -> Self {
213        Self {
214            source: String::new(),
215            value: serde_json::Value::Null,
216            validated: false,
217        }
218    }
219}
220
221/// Context metadata for a finding.
222#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
223pub struct FindingContext {
224    /// Workspace-wide context for this finding
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub workspace: Option<WorkspaceContext>,
227
228    /// Depth of analysis performed
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub analysis_depth: Option<AnalysisDepth>,
231}
232
233/// Workspace-wide context for consensus-based findings.
234#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
235pub struct WorkspaceContext {
236    /// Consensus value across all workspace members
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub consensus_value: Option<serde_json::Value>,
239
240    /// Number of crates with consensus value
241    #[serde(default)]
242    pub consensus_count: u64,
243
244    /// Total number of crates analyzed
245    #[serde(default)]
246    pub total_crates: u64,
247
248    /// Values that differ from consensus
249    #[serde(default, skip_serializing_if = "Vec::is_empty")]
250    pub outliers: Vec<serde_json::Value>,
251
252    /// Crates with outlier values
253    #[serde(default, skip_serializing_if = "Vec::is_empty")]
254    pub outlier_crates: Vec<String>,
255
256    /// Whether all crates agree on the value
257    #[serde(default)]
258    pub all_crates_agree: bool,
259}
260
261/// Depth of analysis performed by the sensor.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
263#[serde(rename_all = "snake_case")]
264pub enum AnalysisDepth {
265    /// Quick scan, may have false negatives
266    Shallow,
267    /// Standard analysis
268    #[default]
269    Full,
270    /// Comprehensive analysis with cross-referencing
271    Deep,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
275#[serde(rename_all = "snake_case")]
276pub enum Severity {
277    #[default]
278    Info,
279    Warn,
280    Error,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct Location {
285    pub path: Utf8PathBuf,
286
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub line: Option<u64>,
289
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub column: Option<u64>,
292}