buildfix_fixer_api/
lib.rs1use anyhow::Result;
2use buildfix_receipts::LoadedReceipt;
3use buildfix_types::ops::SafetyClass;
4use buildfix_types::plan::FindingRef;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Serialize)]
9pub struct FixerMeta {
10 pub fix_key: &'static str,
12 pub description: &'static str,
14 pub safety: SafetyClass,
16 pub consumes_sensors: &'static [&'static str],
18 pub consumes_check_ids: &'static [&'static str],
20}
21
22pub trait RepoView {
24 fn root(&self) -> &camino::Utf8Path;
25
26 fn read_to_string(&self, rel: &camino::Utf8Path) -> Result<String>;
27
28 fn exists(&self, rel: &camino::Utf8Path) -> bool;
29}
30
31#[derive(Debug, Clone, Default)]
33pub struct PlannerConfig {
34 pub allow: Vec<String>,
35 pub deny: Vec<String>,
36 pub allow_guarded: bool,
37 pub allow_unsafe: bool,
38 pub allow_dirty: bool,
39 pub max_ops: Option<u64>,
40 pub max_files: Option<u64>,
41 pub max_patch_bytes: Option<u64>,
42 pub params: std::collections::HashMap<String, String>,
43}
44
45#[derive(Debug, Clone)]
46pub struct PlanContext {
47 pub repo_root: camino::Utf8PathBuf,
48 pub artifacts_dir: camino::Utf8PathBuf,
49 pub config: PlannerConfig,
50}
51
52pub trait Fixer {
54 fn meta(&self) -> FixerMeta;
55
56 fn plan(
57 &self,
58 ctx: &PlanContext,
59 repo: &dyn RepoView,
60 receipts: &ReceiptSet,
61 ) -> anyhow::Result<Vec<buildfix_types::plan::PlanOp>>;
62}
63
64#[derive(Debug, Clone)]
65pub struct MatchedFinding {
66 pub finding: FindingRef,
67 pub data: Option<serde_json::Value>,
68}
69
70#[derive(Debug, Clone)]
71pub struct ReceiptRecord {
72 #[allow(dead_code)]
73 pub sensor_id: String,
74 pub path: camino::Utf8PathBuf,
75 pub envelope: buildfix_types::receipt::ReceiptEnvelope,
76}
77
78#[derive(Debug, Clone)]
80pub struct ReceiptSet {
81 receipts: Vec<ReceiptRecord>,
82}
83
84impl ReceiptSet {
85 pub fn from_loaded(loaded: &[LoadedReceipt]) -> Self {
86 let mut receipts = Vec::new();
87 for r in loaded {
88 if let Ok(env) = &r.receipt {
89 receipts.push(ReceiptRecord {
90 sensor_id: r.sensor_id.clone(),
91 path: r.path.clone(),
92 envelope: env.clone(),
93 });
94 }
95 }
96 receipts.sort_by(|a, b| a.path.cmp(&b.path));
97 Self { receipts }
98 }
99
100 pub fn matching_findings(
101 &self,
102 tool_prefixes: &[&str],
103 check_ids: &[&str],
104 codes: &[&str],
105 ) -> Vec<FindingRef> {
106 self.matching_findings_with_data(tool_prefixes, check_ids, codes)
107 .into_iter()
108 .map(|m| m.finding)
109 .collect()
110 }
111
112 pub fn matching_findings_with_data(
113 &self,
114 tool_prefixes: &[&str],
115 check_ids: &[&str],
116 codes: &[&str],
117 ) -> Vec<MatchedFinding> {
118 let mut out = Vec::new();
119
120 for r in &self.receipts {
121 let tool = r.envelope.tool.name.as_str();
122 if !tool_prefixes.iter().any(|p| tool.starts_with(p)) {
123 continue;
124 }
125
126 for f in &r.envelope.findings {
127 let check_ok = if check_ids.is_empty() {
128 true
129 } else {
130 f.check_id
131 .as_deref()
132 .map(|c| check_ids.contains(&c))
133 .unwrap_or(false)
134 };
135
136 let code_ok = if codes.is_empty() {
137 true
138 } else {
139 f.code
140 .as_deref()
141 .map(|c| codes.contains(&c))
142 .unwrap_or(false)
143 };
144
145 if !check_ok || !code_ok {
146 continue;
147 }
148
149 out.push(MatchedFinding {
150 finding: FindingRef {
151 source: tool.to_string(),
152 check_id: f.check_id.clone(),
153 code: f.code.clone().unwrap_or_else(|| "-".to_string()),
154 path: f.location.as_ref().map(|loc| loc.path.to_string()),
155 line: f.location.as_ref().and_then(|loc| loc.line),
156 fingerprint: f.fingerprint.clone(),
157 },
158 data: f.data.clone(),
159 });
160 }
161 }
162
163 out.sort_by_key(|m| stable_finding_key(&m.finding));
164 out
165 }
166}
167
168fn stable_finding_key(f: &FindingRef) -> String {
169 let loc = f
170 .path
171 .as_ref()
172 .map(|p| format!("{}:{}", p, f.line.unwrap_or(0)))
173 .unwrap_or_else(|| "no_location".to_string());
174
175 format!(
176 "{}/{}/{}|{}",
177 f.source,
178 f.check_id.clone().unwrap_or_default(),
179 f.code,
180 loc
181 )
182}