Skip to main content

IssueRun

Struct IssueRun 

Source
pub struct IssueRun {
    pub issue: i64,
    pub title: String,
    pub status: Status,
    pub pr: Option<String>,
    pub rounds: u32,
    pub disputes: Vec<Dispute>,
    pub filed: Vec<String>,
    pub noted: Vec<Finding>,
    pub notes: Vec<String>,
    pub followup_writes_uncertain: bool,
}
Expand description

The outcome of working one issue, or resuming one PR.

Fields§

§issue: i64§title: String§status: Status§pr: Option<String>§rounds: u32§disputes: Vec<Dispute>§filed: Vec<String>§noted: Vec<Finding>

Real points a reviewer judged smaller than another round.

Nothing else carries them. The round comment is off under the default pr_comments = "outcome" and a non-blocking finding is not filed under the default file_non_blocking = false, so without this the severity ladder is a way to make a finding disappear rather than a way to stop it costing a round. Silence on a pull request should mean nothing was found, not that nothing was gated.

§notes: Vec<String>§followup_writes_uncertain: bool

Runtime circuit breaker after an external follow-up write could not be verified. A later invocation rechecks GitHub before any new write.

Implementations§

Source§

impl IssueRun

Source

pub fn new(issue: i64, title: impl Into<String>) -> Self

Examples found in repository?
examples/preview.rs (line 197)
38fn main() {
39    let loose = std::env::args().any(|a| a == "--loose");
40    let style = if loose {
41        Style {
42            terse: false,
43            ..Style::default()
44        }
45    } else {
46        Style::default()
47    };
48    if loose {
49        println!("(concision gate OFF: this is what a model would post unedited)");
50    }
51
52    rule("A clean review");
53    println!(
54        "{}",
55        review_comment(
56            "codex",
57            1,
58            &Review {
59                verdict: Verdict::Approve,
60                next_action: NextAction::Merge,
61                summary: "I reviewed the changes on this branch carefully and I am happy to \
62                          report that the retry path is correct, the backoff calculation is \
63                          sound, and the new test covers the 429 case that the issue described. \
64                          I have no objections to this change landing as it stands."
65                    .into(),
66                findings: vec![],
67            },
68            &style
69        )
70    );
71
72    rule("A review with real work in it");
73    println!(
74        "{}",
75        review_comment(
76            "codex",
77            2,
78            &Review {
79                verdict: Verdict::ChangesRequested,
80                next_action: NextAction::HandBack,
81                summary: "There is one genuine defect here that should block, along with a \
82                          couple of improvements that I do not think need to gate this \
83                          particular pull request, and one pre-existing problem I noticed \
84                          while reading the surrounding code."
85                    .into(),
86                findings: vec![
87                    finding(
88                        "blocking",
89                        "Retry loop never terminates when max_attempts is unset",
90                        "I confirmed this by running the 429 test with max_attempts left at its \
91                         default of None: the loop spins forever because the guard on line 91 \
92                         compares against Some(0) rather than checking for None first. This is \
93                         not a theoretical concern, the test hangs and I had to kill it.",
94                        "src/net.rs:88",
95                        true,
96                    ),
97                    finding(
98                        "non-blocking",
99                        "The request timeout is hard coded to thirty seconds",
100                        "It would be better if this were configurable, since a slow upstream \
101                         will now fail rather than wait, but the previous code had the same \
102                         limitation so this is not a regression introduced by the change.",
103                        "src/net.rs:44",
104                        true,
105                    ),
106                    finding(
107                        "nit",
108                        "Log line says \"retrying\" without saying how many attempts remain",
109                        "Purely a readability point for whoever is reading the logs at 3am.",
110                        "src/net.rs:102",
111                        true,
112                    ),
113                    finding(
114                        "blocking",
115                        "Config loader swallows a parse error",
116                        "Unrelated to this PR, but load_config discards the error from serde and \
117                         returns Default::default(), so a typo in the config file is silently \
118                         ignored.",
119                        "src/config.rs:210",
120                        false,
121                    ),
122                ],
123            },
124            &style
125        )
126    );
127
128    rule("Answering that review");
129    println!(
130        "{}",
131        disposition_comment(
132            "claude",
133            &ResponseDoc {
134                summary: "One of the two blocking points was right and I have fixed it. I do not \
135                          agree with the other and have explained why below rather than changing \
136                          working code to make the review go away."
137                    .into(),
138                dispositions: vec![],
139            },
140            &["Retry loop never terminates when max_attempts is unset".to_string()],
141            &[
142                "Config loader swallows a parse error. The caller already validates the file \
143               against the schema before load_config is reached, so the discarded error is \
144               unreachable in practice."
145                    .to_string()
146            ],
147            &["https://github.com/you/thing/issues/512".to_string()],
148            &style
149        )
150        .unwrap_or_default()
151    );
152
153    rule("The pull request body");
154    println!(
155        "{}",
156        pr_body(
157            478,
158            &Implementation {
159                summary: "Retry a 429 with exponential backoff instead of failing the request."
160                    .into(),
161                problem: "A rate limited response was treated as fatal, so a single throttled \
162                          call ended a run that had hours of work left in it. The retry path \
163                          existed but only covered connection errors, and nothing in the logs \
164                          said which of the two had happened."
165                    .into(),
166                changes: vec![
167                    "`send` now retries a 429, honouring `Retry-After` when the server sets it \
168                     and backing off exponentially when it does not"
169                        .into(),
170                    "the retry budget is bounded at five attempts, so a permanent 429 still \
171                     ends the call rather than spinning"
172                        .into(),
173                    "a retry logs the status it is retrying, which is what made the original \
174                     failure impossible to tell apart from a dropped connection"
175                        .into(),
176                ],
177                testing: vec![
178                    "`cargo test retries_a_rate_limited_request`, which fakes a 429 with a \
179                     `Retry-After` of 2 and asserts the wait"
180                        .into(),
181                    "point it at a throttled endpoint and watch a run finish rather than stop \
182                     on the first 429"
183                        .into(),
184                ],
185                notes: Some(
186                    "Streaming calls do not go through `send` and are unchanged, which is worth \
187                     a follow-up but not this one."
188                        .into()
189                ),
190                ..Implementation::default()
191            },
192            &style
193        )
194    );
195
196    rule("What a whole run leaves on the PR (the default, one comment)");
197    let mut ended = IssueRun::new(482, "t");
198    ended.disputes = vec![Dispute {
199        title: "Config loader swallows a parse error".into(),
200        file: "src/config.rs".into(),
201        reasoning: "the caller validates against the schema before load_config is reached".into(),
202    }];
203    ended.filed = vec![
204        "https://github.com/you/thing/issues/485".into(),
205        "https://github.com/you/thing/issues/486".into(),
206    ];
207    println!(
208        "{}",
209        outcome_comment(
210            &ended,
211            &spar::model::Ledger::new(),
212            &Ending::OutOfRounds,
213            &style
214        )
215        .unwrap_or_default()
216    );
217    println!("\n  (and a clean run that filed nothing posts no comment at all)");
218
219    rule("What a run leaves when the closing pass did not sign it off");
220    let mut left = IssueRun::new(482, "t");
221    left.noted = vec![finding(
222        "non-blocking",
223        "Timeout is not configurable",
224        "The retry budget is fixed at three.",
225        "src/net.rs",
226        true,
227    )];
228    let unresolved = vec![finding(
229        "blocking",
230        "The retry fix never reaches the 429 path",
231        "The guard added in round 2 sits after the early return on line 88, so a rate limited \
232         response still takes the old path. Reproduced with the 429 test.",
233        "src/net.rs:88",
234        true,
235    )];
236    println!(
237        "{}",
238        outcome_comment(
239            &left,
240            &spar::model::Ledger::new(),
241            &Ending::Unresolved(&unresolved),
242            &style
243        )
244        .unwrap_or_default()
245    );
246
247    rule("A review of somebody else's pull request (spar review)");
248    let judged = |standing, severity, title: &str, detail: &str, file: &str, by: &str| Judged {
249        finding: finding(severity, title, detail, file, true),
250        raised_by: by.to_string(),
251        standing,
252        counterpoint: None,
253        defence: None,
254    };
255    let mut disputed = judged(
256        Standing::Disputed,
257        "blocking",
258        "Config loader swallows a parse error",
259        "load_config discards the error from serde and returns a default.",
260        "src/config.rs:210",
261        "claude",
262    );
263    disputed.counterpoint =
264        Some("the caller validates against the schema before load_config is reached".into());
265    println!(
266        "{}",
267        verdict_comment(
268            &[
269                judged(
270                    Standing::Corroborated,
271                    "blocking",
272                    "Retry loop never terminates when max_attempts is unset",
273                    "Both reviewers reproduced this: the guard on line 91 compares against \
274                     Some(0) rather than checking for None, so the 429 test hangs.",
275                    "src/net.rs:88",
276                    "claude and codex",
277                ),
278                judged(
279                    Standing::Confirmed,
280                    "non-blocking",
281                    "The request timeout is hard coded",
282                    "Not a regression, the previous code had the same limitation.",
283                    "src/net.rs:44",
284                    "codex",
285                ),
286                judged(
287                    Standing::Unverified,
288                    "nit",
289                    "Log line does not say how many attempts remain",
290                    "Readability for whoever reads the logs at 3am.",
291                    "src/net.rs:102",
292                    "claude",
293                ),
294                disputed,
295                judged(
296                    Standing::Withdrawn,
297                    "blocking",
298                    "Off by one in the backoff",
299                    "Withdrawn after the other reviewer pointed at the test that covers it.",
300                    "src/net.rs:70",
301                    "codex",
302                ),
303            ],
304            &style
305        )
306    );
307
308    rule("An issue both reviewers declined");
309    println!(
310        "{}",
311        skip_comment(
312            &SkippedItem {
313                issue: 91,
314                title: "Add a dark mode".into(),
315                tracker: false,
316                reasons: [
317                    (
318                        "claude".to_string(),
319                        "This was already implemented in 1.4 and shipped behind the theme \
320                         setting, so there is nothing left to do here."
321                            .to_string()
322                    ),
323                    (
324                        "codex".to_string(),
325                        "Duplicate of #62, which is still open and has the full discussion."
326                            .to_string()
327                    ),
328                ]
329                .into_iter()
330                .collect(),
331            },
332            &style
333        )
334    );
335    rule("Answering a comment in its own thread");
336    for item in [
337        settled(
338            Ask::Implement,
339            "alice",
340            "src/retry.rs:91",
341            "Added the guard on the retry path and a test that reproduces the original failure.",
342            "",
343            true,
344        ),
345        settled(
346            Ask::Decline,
347            "bob",
348            "src/pool.rs:14",
349            "",
350            "Every caller of this function already holds the pool lock, so the check on line 14 \
351             cannot be reached with a null connection. I ran the suite with an assertion in its \
352             place and nothing tripped it.",
353            false,
354        ),
355        settled(
356            Ask::Defer,
357            "carol",
358            "src/electrum.rs:203",
359            "",
360            "This is real and it predates the branch: the reconnect monitor puts a stopped wallet \
361             back into the header router. It is not caused by anything here, so fixing it in this \
362             pull request would put an unrelated change in front of whoever reviews it.",
363            false,
364        ),
365    ] {
366        println!("{}\n", thread_reply(&item, &style));
367    }
368
369    rule("What a check-in leaves on the pull request");
370    let mut fixed = settled(
371        Ask::Implement,
372        "alice",
373        "the pull request",
374        "Retried the 429 with the backoff the issue asked for.",
375        "",
376        true,
377    );
378    fixed.pending.file = None;
379    let mut parked = settled(
380        Ask::Decline,
381        "dave",
382        "the pull request",
383        "",
384        "The two reviewers read the same code and did not agree about whether the guard is \
385         reachable.",
386        false,
387    );
388    parked.pending.file = None;
389    parked.parked = true;
390    println!(
391        "{}",
392        checkin_comment(&[fixed, parked], &style)
393            .unwrap_or_else(|| "(nothing to say, so no comment is posted)".into())
394    );
395    println!("\n  (and a check-in with nothing outstanding posts no comment at all)");
396
397    println!();
398}
Source

pub fn succeeded(&self) -> bool

Whether this outcome counts as the run having done its job.

A review that produced findings did its job: the findings are the product, and a PR needing work is not a failure of the reviewer.

Trait Implementations§

Source§

impl Clone for IssueRun

Source§

fn clone(&self) -> IssueRun

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for IssueRun

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for IssueRun

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for IssueRun

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.