1use spar::model::{
11 Dispute, Finding, IssueRun, Judged, NextAction, ResponseDoc, Review, Severity, SkippedItem,
12 Standing, Verdict,
13};
14use spar::review::{
15 disposition_comment, outcome_comment, pr_body, review_comment, skip_comment, Ending,
16};
17use spar::review_only::verdict_comment;
18use spar::style::Style;
19
20fn finding(severity: &str, title: &str, detail: &str, file: &str, in_scope: bool) -> Finding {
21 Finding {
22 severity: Severity::parse_lenient(severity).expect("severity"),
23 title: title.into(),
24 detail: detail.into(),
25 file: file.into(),
26 in_scope,
27 ..Default::default()
28 }
29}
30
31fn rule(label: &str) {
32 println!("\n\x1b[1m{label}\x1b[0m\n{}", "-".repeat(72));
33}
34
35fn main() {
36 let loose = std::env::args().any(|a| a == "--loose");
37 let style = if loose {
38 Style {
39 terse: false,
40 ..Style::default()
41 }
42 } else {
43 Style::default()
44 };
45 if loose {
46 println!("(concision gate OFF: this is what a model would post unedited)");
47 }
48
49 rule("A clean review");
50 println!(
51 "{}",
52 review_comment(
53 "codex",
54 1,
55 &Review {
56 verdict: Verdict::Approve,
57 next_action: NextAction::Merge,
58 summary: "I reviewed the changes on this branch carefully and I am happy to \
59 report that the retry path is correct, the backoff calculation is \
60 sound, and the new test covers the 429 case that the issue described. \
61 I have no objections to this change landing as it stands."
62 .into(),
63 findings: vec![],
64 },
65 &style
66 )
67 );
68
69 rule("A review with real work in it");
70 println!(
71 "{}",
72 review_comment(
73 "codex",
74 2,
75 &Review {
76 verdict: Verdict::ChangesRequested,
77 next_action: NextAction::HandBack,
78 summary: "There is one genuine defect here that should block, along with a \
79 couple of improvements that I do not think need to gate this \
80 particular pull request, and one pre-existing problem I noticed \
81 while reading the surrounding code."
82 .into(),
83 findings: vec![
84 finding(
85 "blocking",
86 "Retry loop never terminates when max_attempts is unset",
87 "I confirmed this by running the 429 test with max_attempts left at its \
88 default of None: the loop spins forever because the guard on line 91 \
89 compares against Some(0) rather than checking for None first. This is \
90 not a theoretical concern, the test hangs and I had to kill it.",
91 "src/net.rs:88",
92 true,
93 ),
94 finding(
95 "non-blocking",
96 "The request timeout is hard coded to thirty seconds",
97 "It would be better if this were configurable, since a slow upstream \
98 will now fail rather than wait, but the previous code had the same \
99 limitation so this is not a regression introduced by the change.",
100 "src/net.rs:44",
101 true,
102 ),
103 finding(
104 "nit",
105 "Log line says \"retrying\" without saying how many attempts remain",
106 "Purely a readability point for whoever is reading the logs at 3am.",
107 "src/net.rs:102",
108 true,
109 ),
110 finding(
111 "blocking",
112 "Config loader swallows a parse error",
113 "Unrelated to this PR, but load_config discards the error from serde and \
114 returns Default::default(), so a typo in the config file is silently \
115 ignored.",
116 "src/config.rs:210",
117 false,
118 ),
119 ],
120 },
121 &style
122 )
123 );
124
125 rule("Answering that review");
126 println!(
127 "{}",
128 disposition_comment(
129 "claude",
130 &ResponseDoc {
131 summary: "One of the two blocking points was right and I have fixed it. I do not \
132 agree with the other and have explained why below rather than changing \
133 working code to make the review go away."
134 .into(),
135 dispositions: vec![],
136 },
137 &["Retry loop never terminates when max_attempts is unset".to_string()],
138 &[
139 "Config loader swallows a parse error. The caller already validates the file \
140 against the schema before load_config is reached, so the discarded error is \
141 unreachable in practice."
142 .to_string()
143 ],
144 &["https://github.com/you/thing/issues/512".to_string()],
145 &style
146 )
147 .unwrap_or_default()
148 );
149
150 rule("The pull request body");
151 println!(
152 "{}",
153 pr_body(
154 478,
155 "Retry a 429 with exponential backoff instead of failing the request.",
156 &style
157 )
158 );
159
160 rule("What a whole run leaves on the PR (the default, one comment)");
161 let mut ended = IssueRun::new(482, "t");
162 ended.disputes = vec![Dispute {
163 title: "Config loader swallows a parse error".into(),
164 reasoning: "the caller validates against the schema before load_config is reached".into(),
165 }];
166 ended.filed = vec![
167 "https://github.com/you/thing/issues/485".into(),
168 "https://github.com/you/thing/issues/486".into(),
169 ];
170 println!(
171 "{}",
172 outcome_comment(
173 &ended,
174 &spar::model::Ledger::new(),
175 &Ending::OutOfRounds,
176 &style
177 )
178 .unwrap_or_default()
179 );
180 println!("\n (and a clean run that filed nothing posts no comment at all)");
181
182 rule("A review of somebody else's pull request (spar review)");
183 let judged = |standing, severity, title: &str, detail: &str, file: &str, by: &str| Judged {
184 finding: finding(severity, title, detail, file, true),
185 raised_by: by.to_string(),
186 standing,
187 counterpoint: None,
188 defence: None,
189 };
190 let mut disputed = judged(
191 Standing::Disputed,
192 "blocking",
193 "Config loader swallows a parse error",
194 "load_config discards the error from serde and returns a default.",
195 "src/config.rs:210",
196 "claude",
197 );
198 disputed.counterpoint =
199 Some("the caller validates against the schema before load_config is reached".into());
200 println!(
201 "{}",
202 verdict_comment(
203 &[
204 judged(
205 Standing::Corroborated,
206 "blocking",
207 "Retry loop never terminates when max_attempts is unset",
208 "Both reviewers reproduced this: the guard on line 91 compares against \
209 Some(0) rather than checking for None, so the 429 test hangs.",
210 "src/net.rs:88",
211 "claude and codex",
212 ),
213 judged(
214 Standing::Confirmed,
215 "non-blocking",
216 "The request timeout is hard coded",
217 "Not a regression, the previous code had the same limitation.",
218 "src/net.rs:44",
219 "codex",
220 ),
221 judged(
222 Standing::Unverified,
223 "nit",
224 "Log line does not say how many attempts remain",
225 "Readability for whoever reads the logs at 3am.",
226 "src/net.rs:102",
227 "claude",
228 ),
229 disputed,
230 judged(
231 Standing::Withdrawn,
232 "blocking",
233 "Off by one in the backoff",
234 "Withdrawn after the other reviewer pointed at the test that covers it.",
235 "src/net.rs:70",
236 "codex",
237 ),
238 ],
239 &style
240 )
241 );
242
243 rule("An issue both reviewers declined");
244 println!(
245 "{}",
246 skip_comment(
247 &SkippedItem {
248 issue: 91,
249 title: "Add a dark mode".into(),
250 reasons: [
251 (
252 "claude".to_string(),
253 "This was already implemented in 1.4 and shipped behind the theme \
254 setting, so there is nothing left to do here."
255 .to_string()
256 ),
257 (
258 "codex".to_string(),
259 "Duplicate of #62, which is still open and has the full discussion."
260 .to_string()
261 ),
262 ]
263 .into_iter()
264 .collect(),
265 },
266 &style
267 )
268 );
269 println!();
270}