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