1use crate::run::{E2eStatus, ReviewRound, RunState};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FinalHead<'a> {
18 Unreported,
20 Unknown,
22 At(&'a str),
24}
25
26fn short(sha: &str) -> &str {
27 sha.get(..7).unwrap_or(sha)
28}
29
30fn all_ok(outcomes: &[crate::run::CommandOutcome]) -> bool {
31 !outcomes.is_empty() && outcomes.iter().all(|o| o.ok())
32}
33
34pub fn review_rounds(state: &RunState) -> String {
37 let mut out = String::new();
38 for r in &state.reviews {
39 let Some(fix) = &r.fix else { continue };
40 if out.is_empty() {
41 out.push_str("## Review rounds\n\n");
42 }
43 out.push_str(&format!("### Round {}\n\n", r.round));
44 out.push_str(if fix.committed {
45 "The fix is committed on the branch (including work committed by magi).\n\n"
46 } else {
47 "This round produced no commit.\n\n"
48 });
49 for id in &fix.addressed {
50 out.push_str(&format!("- Addressed: `{id}`\n"));
51 }
52 for rejection in &fix.rejected {
53 out.push_str(&format!("- Declined in this round: `{}`\n", rejection.id));
54 }
55 out.push('\n');
56 }
57 out
58}
59
60pub fn summary_evidence(state: &RunState, head: FinalHead<'_>) -> String {
63 let open = state.last_round_findings();
64 let declined: Vec<_> = state
65 .reviews
66 .iter()
67 .filter_map(|r| r.fix.as_ref())
68 .flat_map(|f| &f.rejected)
69 .map(|rejection| {
70 let finding = state
71 .reviews
72 .iter()
73 .flat_map(|r| &r.reviews)
74 .flat_map(|r| &r.findings)
75 .find(|f| f.id == rejection.id);
76 serde_json::json!({"id": rejection.id, "reason": rejection.why,
77 "finding": finding})
78 })
79 .collect();
80 let verification = serde_json::json!({
81 "gate": state.gate.iter().map(|o| (&o.command, o.code, o.resource_blocked)).collect::<Vec<_>>(),
82 "tests": state.reviews.iter().map(|r| (r.round, &r.verified_head,
83 r.e2e.iter().map(|o| (&o.command, o.code, o.resource_blocked)).collect::<Vec<_>>())).collect::<Vec<_>>()
84 });
85 format!(
86 "{}{}\nOpen findings (local source material):\n{}\nDeclined findings (local source material):\n{}\nVerification commands and outcomes:\n{verification}",
87 review_rounds(state),
88 final_verification(state, head),
89 serde_json::to_string(&open).unwrap_or_default(),
90 serde_json::to_string(&declined).unwrap_or_default()
91 )
92}
93
94pub fn summary_fingerprint(state: &RunState, evidence: &str) -> String {
96 crate::github_text::fingerprint(
97 &format!(
98 "public-description-v3:{}",
99 state.config.graph.github_language
100 ),
101 evidence,
102 )
103}
104
105pub fn required_finding_ids(state: &RunState) -> Vec<String> {
107 let mut ids: Vec<_> = state
108 .last_round_findings()
109 .iter()
110 .map(|f| f.id.clone())
111 .collect();
112 ids.extend(
113 state
114 .reviews
115 .iter()
116 .filter_map(|r| r.fix.as_ref())
117 .flat_map(|f| f.rejected.iter().map(|r| r.id.clone())),
118 );
119 ids.sort();
120 ids.dedup();
121 ids
122}
123
124pub fn covers_required_findings(state: &RunState, prose: &str) -> bool {
127 required_finding_ids(state).iter().all(|id| {
128 prose
129 .split(|c: char| !(c.is_ascii_alphanumeric() || c == '-'))
130 .any(|word| word == id)
131 })
132}
133
134fn last_checked(state: &RunState) -> Option<&ReviewRound> {
135 state
136 .reviews
137 .iter()
138 .rev()
139 .find(|r| r.e2e_status() != E2eStatus::NotConfigured)
140}
141
142pub fn final_verification(state: &RunState, head: FinalHead<'_>) -> String {
145 if head == FinalHead::Unreported {
146 return String::new();
147 }
148 let at = match head {
149 FinalHead::At(h) => Some(h),
150 _ => None,
151 };
152 let mut lines = vec![match at {
153 Some(h) => format!("- Final commit: `{}`", short(h)),
154 None => "- Final commit: unknown, so no check below is claimed for it".to_owned(),
155 }];
156 let ran_on = |recorded: Option<&str>| -> String {
157 match (at, recorded) {
158 (Some(h), Some(r)) if h == r => "on the final commit".to_owned(),
159 (_, Some(r)) => format!(
160 "on an earlier commit (`{}`); later changes were not re-verified by it",
161 short(r)
162 ),
163 (_, None) => "on a commit that was not recorded".to_owned(),
164 }
165 };
166 match last_checked(state) {
167 None => lines.push("- Tests (`verify.e2e`): not configured or never run".to_owned()),
168 Some(r) => lines.push(match r.e2e_status() {
169 E2eStatus::Passed => format!(
170 "- Tests (`verify.e2e`): passed {} (round {})",
171 ran_on(r.verified_head.as_deref()),
172 r.round
173 ),
174 E2eStatus::Failed => format!(
175 "- Tests (`verify.e2e`): FAILED {} (round {})",
176 ran_on(r.verified_head.as_deref()),
177 r.round
178 ),
179 E2eStatus::Deferred => format!(
180 "- Tests (`verify.e2e`): deferred in round {}, not run",
181 r.round
182 ),
183 E2eStatus::ResourceBlocked => format!(
184 "- Tests (`verify.e2e`): not run in round {} (blocked on a shared resource)",
185 r.round
186 ),
187 E2eStatus::NotConfigured => unreachable!(),
188 }),
189 }
190 lines.push(if !state.gate_ran {
191 "- Gate (`verify.gate`): not run".to_owned()
192 } else if state.gate.is_empty() {
193 "- Gate (`verify.gate`): no commands configured".to_owned()
194 } else if !all_ok(&state.gate) {
195 format!(
196 "- Gate (`verify.gate`): FAILED {}",
197 ran_on(state.gate_head.as_deref())
198 )
199 } else {
200 format!(
201 "- Gate (`verify.gate`): passed {}",
202 ran_on(state.gate_head.as_deref())
203 )
204 });
205 if !state.gate_fixes.is_empty() {
206 lines.push(format!(
207 "- Gate-fix rounds spent: {}",
208 state.gate_fixes.len()
209 ));
210 }
211 if !state.rebase_fixes.is_empty() {
212 lines.push(format!(
213 "- Rebase conflict rounds spent: {}",
214 state.rebase_fixes.len()
215 ));
216 }
217 format!("## Final verification\n\n{}\n\n", lines.join("\n"))
218}