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: boolRuntime 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
impl IssueRun
Sourcepub fn new(issue: i64, title: impl Into<String>) -> Self
pub fn new(issue: i64, title: impl Into<String>) -> Self
Examples found in repository?
examples/preview.rs (line 199)
39fn main() {
40 let loose = std::env::args().any(|a| a == "--loose");
41 let style = if loose {
42 Style {
43 terse: false,
44 ..Style::default()
45 }
46 } else {
47 Style::default()
48 };
49 if loose {
50 println!("(concision gate OFF: this is what a model would post unedited)");
51 }
52
53 rule("A clean review");
54 println!(
55 "{}",
56 review_comment(
57 "codex",
58 1,
59 &Review {
60 verdict: Verdict::Approve,
61 next_action: NextAction::Merge,
62 summary: "I reviewed the changes on this branch carefully and I am happy to \
63 report that the retry path is correct, the backoff calculation is \
64 sound, and the new test covers the 429 case that the issue described. \
65 I have no objections to this change landing as it stands."
66 .into(),
67 findings: vec![],
68 },
69 &style
70 )
71 );
72
73 rule("A review with real work in it");
74 println!(
75 "{}",
76 review_comment(
77 "codex",
78 2,
79 &Review {
80 verdict: Verdict::ChangesRequested,
81 next_action: NextAction::HandBack,
82 summary: "There is one genuine defect here that should block, along with a \
83 couple of improvements that I do not think need to gate this \
84 particular pull request, and one pre-existing problem I noticed \
85 while reading the surrounding code."
86 .into(),
87 findings: vec![
88 finding(
89 "blocking",
90 "Retry loop never terminates when max_attempts is unset",
91 "I confirmed this by running the 429 test with max_attempts left at its \
92 default of None: the loop spins forever because the guard on line 91 \
93 compares against Some(0) rather than checking for None first. This is \
94 not a theoretical concern, the test hangs and I had to kill it.",
95 "src/net.rs:88",
96 true,
97 ),
98 finding(
99 "non-blocking",
100 "The request timeout is hard coded to thirty seconds",
101 "It would be better if this were configurable, since a slow upstream \
102 will now fail rather than wait, but the previous code had the same \
103 limitation so this is not a regression introduced by the change.",
104 "src/net.rs:44",
105 true,
106 ),
107 finding(
108 "nit",
109 "Log line says \"retrying\" without saying how many attempts remain",
110 "Purely a readability point for whoever is reading the logs at 3am.",
111 "src/net.rs:102",
112 true,
113 ),
114 finding(
115 "blocking",
116 "Config loader swallows a parse error",
117 "Unrelated to this PR, but load_config discards the error from serde and \
118 returns Default::default(), so a typo in the config file is silently \
119 ignored.",
120 "src/config.rs:210",
121 false,
122 ),
123 ],
124 },
125 &style
126 )
127 );
128
129 rule("Answering that review");
130 println!(
131 "{}",
132 disposition_comment(
133 "claude",
134 &ResponseDoc {
135 summary: "One of the two blocking points was right and I have fixed it. I do not \
136 agree with the other and have explained why below rather than changing \
137 working code to make the review go away."
138 .into(),
139 dispositions: vec![],
140 },
141 &["Retry loop never terminates when max_attempts is unset".to_string()],
142 &[
143 "Config loader swallows a parse error. The caller already validates the file \
144 against the schema before load_config is reached, so the discarded error is \
145 unreachable in practice."
146 .to_string()
147 ],
148 &["https://github.com/you/thing/issues/512".to_string()],
149 &style
150 )
151 .unwrap_or_default()
152 );
153
154 rule("The pull request body");
155 println!(
156 "{}",
157 pr_body(
158 478,
159 &Implementation {
160 summary: "Retry a 429 with exponential backoff instead of failing the request."
161 .into(),
162 problem: "A rate limited response was treated as fatal, so a single throttled \
163 call ended a run that had hours of work left in it. The retry path \
164 existed but only covered connection errors, and nothing in the logs \
165 said which of the two had happened."
166 .into(),
167 changes: vec![
168 "`send` now retries a 429, honouring `Retry-After` when the server sets it \
169 and backing off exponentially when it does not"
170 .into(),
171 "the retry budget is bounded at five attempts, so a permanent 429 still \
172 ends the call rather than spinning"
173 .into(),
174 "a retry logs the status it is retrying, which is what made the original \
175 failure impossible to tell apart from a dropped connection"
176 .into(),
177 ],
178 testing: vec![
179 "`cargo test retries_a_rate_limited_request`, which fakes a 429 with a \
180 `Retry-After` of 2 and asserts the wait"
181 .into(),
182 "point it at a throttled endpoint and watch a run finish rather than stop \
183 on the first 429"
184 .into(),
185 ],
186 notes: Some(
187 "Streaming calls do not go through `send` and are unchanged, which is worth \
188 a follow-up but not this one."
189 .into()
190 ),
191 ..Implementation::default()
192 },
193 Some("cargo test"),
194 &style
195 )
196 );
197
198 rule("What a whole run leaves on the PR (the default, one comment)");
199 let mut ended = IssueRun::new(482, "t");
200 ended.disputes = vec![Dispute {
201 title: "Config loader swallows a parse error".into(),
202 file: "src/config.rs".into(),
203 reasoning: "the caller validates against the schema before load_config is reached".into(),
204 }];
205 ended.filed = vec![
206 "https://github.com/you/thing/issues/485".into(),
207 "https://github.com/you/thing/issues/486".into(),
208 ];
209 println!(
210 "{}",
211 outcome_comment(
212 &ended,
213 &spar::model::Ledger::new(),
214 &Ending::OutOfRounds,
215 &style
216 )
217 .unwrap_or_default()
218 );
219 println!("\n (and a clean run that filed nothing posts no comment at all)");
220
221 rule("What a run leaves when the closing pass did not sign it off");
222 let mut left = IssueRun::new(482, "t");
223 left.noted = vec![finding(
224 "non-blocking",
225 "Timeout is not configurable",
226 "The retry budget is fixed at three.",
227 "src/net.rs",
228 true,
229 )];
230 let unresolved = vec![finding(
231 "blocking",
232 "The retry fix never reaches the 429 path",
233 "The guard added in round 2 sits after the early return on line 88, so a rate limited \
234 response still takes the old path. Reproduced with the 429 test.",
235 "src/net.rs:88",
236 true,
237 )];
238 println!(
239 "{}",
240 outcome_comment(
241 &left,
242 &spar::model::Ledger::new(),
243 &Ending::Unresolved(&unresolved),
244 &style
245 )
246 .unwrap_or_default()
247 );
248
249 rule("A review of somebody else's pull request (spar review)");
250 let judged = |standing, severity, title: &str, detail: &str, file: &str, by: &str| Judged {
251 finding: finding(severity, title, detail, file, true),
252 raised_by: by.to_string(),
253 standing,
254 counterpoint: None,
255 defence: None,
256 rated: None,
257 };
258 let mut disputed = judged(
259 FindingStanding::Disputed,
260 "blocking",
261 "Config loader swallows a parse error",
262 "load_config discards the error from serde and returns a default.",
263 "src/config.rs:210",
264 "claude",
265 );
266 disputed.counterpoint =
267 Some("the caller validates against the schema before load_config is reached".into());
268 println!(
269 "{}",
270 verdict_comment(
271 &[
272 judged(
273 FindingStanding::Corroborated,
274 "blocking",
275 "Retry loop never terminates when max_attempts is unset",
276 "Both reviewers reproduced this: the guard on line 91 compares against \
277 Some(0) rather than checking for None, so the 429 test hangs.",
278 "src/net.rs:88",
279 "claude and codex",
280 ),
281 judged(
282 FindingStanding::Confirmed,
283 "non-blocking",
284 "The request timeout is hard coded",
285 "Not a regression, the previous code had the same limitation.",
286 "src/net.rs:44",
287 "codex",
288 ),
289 judged(
290 FindingStanding::Unverified,
291 "nit",
292 "Log line does not say how many attempts remain",
293 "Readability for whoever reads the logs at 3am.",
294 "src/net.rs:102",
295 "claude",
296 ),
297 disputed,
298 judged(
299 FindingStanding::Withdrawn,
300 "blocking",
301 "Off by one in the backoff",
302 "Withdrawn after the other reviewer pointed at the test that covers it.",
303 "src/net.rs:70",
304 "codex",
305 ),
306 ],
307 &style
308 )
309 );
310
311 rule("A second look at that pull request, after the author pushed (spar review --recheck)");
312 let recorded = |severity: &str, title: &str, detail: &str, file: &str, standing| {
313 spar::model::RecordedFinding {
314 title: title.into(),
315 file: file.into(),
316 severity: Severity::parse_lenient(severity).expect("severity"),
317 detail: detail.into(),
318 in_scope: true,
319 raised_by: "claude and codex".into(),
320 standing,
321 counterpoint: None,
322 defence: None,
323 }
324 };
325 println!(
326 "{}",
327 recheck_comment(
328 &Delta {
329 from: "0123abcdef".into(),
330 commits: vec![
331 "a1b2c3d check for None before comparing attempts".into(),
332 "e4f5a6b make the request timeout configurable".into(),
333 ],
334 rewritten: false,
335 },
336 &[
337 Rechecked {
338 recorded: recorded(
339 "blocking",
340 "Retry loop never terminates when max_attempts is unset",
341 "The guard on line 91 compares against Some(0) rather than checking for None.",
342 "src/net.rs:88",
343 FindingStanding::Corroborated,
344 ),
345 resolution: Resolution::Fixed(
346 "src/net.rs:91 now matches on None first, and the 429 test that used to \
347 hang passes in 0.3s when run with cargo test retry"
348 .into(),
349 ),
350 },
351 Rechecked {
352 recorded: recorded(
353 "non-blocking",
354 "The request timeout is hard coded",
355 "Not a regression, the previous code had the same limitation.",
356 "src/net.rs:44",
357 FindingStanding::Confirmed,
358 ),
359 resolution: Resolution::Rebutted(
360 "the author's reply is right that the timeout comes from the caller's \
361 Client builder since e4f5a6b, so there is nothing left to make configurable \
362 here"
363 .into(),
364 ),
365 },
366 Rechecked {
367 recorded: recorded(
368 "blocking",
369 "Config loader swallows a parse error",
370 "load_config discards the error from serde and returns a default.",
371 "src/config.rs:210",
372 FindingStanding::Confirmed,
373 ),
374 resolution: Resolution::Open(
375 "codex still sees it: load_config still returns Default on a parse error, \
376 and nothing upstream validates the file first"
377 .into(),
378 ),
379 },
380 ],
381 &[judged(
382 FindingStanding::Corroborated,
383 "non-blocking",
384 "The new None branch logs at error level on every retry",
385 "Each retry now writes an error line, so a flaky upstream fills the log with \
386 errors for a request that succeeds on the third try.",
387 "src/net.rs:93",
388 "claude and codex",
389 )],
390 &style,
391 2
392 )
393 );
394
395 rule("An issue both reviewers declined");
396 println!(
397 "{}",
398 skip_comment(
399 &SkippedItem {
400 issue: 91,
401 title: "Add a dark mode".into(),
402 tracker: false,
403 reasons: [
404 (
405 "claude".to_string(),
406 "This was already implemented in 1.4 and shipped behind the theme \
407 setting, so there is nothing left to do here."
408 .to_string()
409 ),
410 (
411 "codex".to_string(),
412 "Duplicate of #62, which is still open and has the full discussion."
413 .to_string()
414 ),
415 ]
416 .into_iter()
417 .collect(),
418 },
419 &style
420 )
421 );
422 rule("Answering a comment in its own thread");
423 for item in [
424 settled(
425 Ask::Implement,
426 "alice",
427 "src/retry.rs:91",
428 "Added the guard on the retry path and a test that reproduces the original failure.",
429 "",
430 true,
431 ),
432 settled(
433 Ask::Decline,
434 "bob",
435 "src/pool.rs:14",
436 "",
437 "Every caller of this function already holds the pool lock, so the check on line 14 \
438 cannot be reached with a null connection. I ran the suite with an assertion in its \
439 place and nothing tripped it.",
440 false,
441 ),
442 settled(
443 Ask::Defer,
444 "carol",
445 "src/electrum.rs:203",
446 "",
447 "This is real and it predates the branch: the reconnect monitor puts a stopped wallet \
448 back into the header router. It is not caused by anything here, so fixing it in this \
449 pull request would put an unrelated change in front of whoever reviews it.",
450 false,
451 ),
452 ] {
453 println!("{}\n", thread_reply(&item, &style));
454 }
455
456 rule("What a check-in leaves on the pull request");
457 let mut fixed = settled(
458 Ask::Implement,
459 "alice",
460 "the pull request",
461 "Retried the 429 with the backoff the issue asked for.",
462 "",
463 true,
464 );
465 fixed.pending.file = None;
466 let mut parked = settled(
467 Ask::Decline,
468 "dave",
469 "the pull request",
470 "",
471 "The two reviewers read the same code and did not agree about whether the guard is \
472 reachable.",
473 false,
474 );
475 parked.pending.file = None;
476 parked.parked = true;
477 println!(
478 "{}",
479 checkin_comment(&[fixed, parked], &style)
480 .unwrap_or_else(|| "(nothing to say, so no comment is posted)".into())
481 );
482 println!("\n (and a check-in with nothing outstanding posts no comment at all)");
483
484 rule("An idea filed from a brainstorm");
485 let idea = Candidate {
486 id: 1,
487 idea: Idea {
488 title: "Batch settlement with hash timelocks".into(),
489 combines: vec![
490 "hash time locked contracts".into(),
491 "batch auctions".into(),
492 "the OP_CHECKSEQUENCEVERIFY delay".into(),
493 ],
494 how_it_works: "Each trade posts a hash timelock, and one batch auction clears every \
495 one of them at the deadline together, so the batch is the settlement."
496 .into(),
497 why_new: "The nearest thing is a plain escrow, which settles one trade at a time \
498 and cannot net them against each other."
499 .into(),
500 enables: "Settling many trades in one transaction.".into(),
501 risks: "A fee spike at the deadline makes the batch uneconomic.".into(),
502 first_experiment: "Simulate ten trades on regtest and count the bytes.".into(),
503 },
504 proposers: vec!["claude".into()],
505 parent: None,
506 objection: Some((
507 "codex".into(),
508 "Coinjoin coordinators already batch settlement.".into(),
509 )),
510 defence: Some(
511 "A coordinator batches inputs, not conditional trades; nothing there clears a hash \
512 lock."
513 .into(),
514 ),
515 standing: Standing::Defended,
516 rank_sum: 3,
517 };
518 println!("{}", issue_body(&idea, &style));
519 println!("\n (who proposed and who objected stays in the session file, never in the issue)");
520
521 println!();
522}Trait Implementations§
Source§impl<'de> Deserialize<'de> for IssueRun
impl<'de> Deserialize<'de> for IssueRun
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Deserialize this value from the given Serde deserializer. Read more
Auto Trait Implementations§
impl Freeze for IssueRun
impl RefUnwindSafe for IssueRun
impl Send for IssueRun
impl Sync for IssueRun
impl Unpin for IssueRun
impl UnsafeUnpin for IssueRun
impl UnwindSafe for IssueRun
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more