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