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    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}