Skip to main content

preview/
preview.rs

1//! Render every comment spar can post, so you can see what lands on GitHub
2//! before you spend a token.
3//!
4//!     cargo run --example preview
5//!     cargo run --example preview -- --loose    # with the concision gate off
6//!
7//! The model output below is deliberately as verbose as a real model gets. What
8//! prints is what a reviewer would actually read.
9
10use 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}