Skip to main content

speccoder_runtime/bug/
handoff.rs

1//! The hand-off of a bug that stopped before a fix, composed by the engine
2//! from what the tries recorded — never written by a model. The author's
3//! last words appear only as a labelled claim.
4
5use speccoder_core::bug::{BugReport, Claim, Handoff, Outcome, Statements};
6use speccoder_core::next::NextStep;
7use speccoder_core::verify::RedClass;
8
9/// What the person can do next with `bug` in `outcome`.
10#[must_use]
11pub fn next(bug: &BugReport, outcome: Outcome) -> Vec<NextStep> {
12    let key = bug.key();
13    let step = |command: String, why: &str| NextStep {
14        command,
15        why: why.to_owned(),
16    };
17    let reword = || {
18        if bug.home.kind.native() {
19            step(
20                format!(
21                    "speccoder bug --slug {}",
22                    bug.task.trim_start_matches("bug:")
23                ),
24                "after rewording the assessment's statements",
25            )
26        } else {
27            step(
28                format!(
29                    "speccoder review edit {} --feature {} \"…\"",
30                    bug.requirement, bug.unit
31                ),
32                "reword the triad (the confirmation lapses)",
33            )
34        }
35    };
36    match outcome {
37        Outcome::CannotReproduce | Outcome::UnchangedNotHolding => vec![
38            step(
39                format!(
40                    "speccoder bug \"{}\" --expected \"…\" --test <file>::<test>",
41                    bug.triad.current.text
42                ),
43                "bring a failing test yourself",
44            ),
45            reword(),
46            step(
47                format!("speccoder bug run --task {key}"),
48                "try reproducing again",
49            ),
50            step(
51                format!(
52                    "speccoder task skip {} --feature {} --reason \"…\"",
53                    bug.task, bug.unit
54                ),
55                "drop it",
56            ),
57        ],
58        Outcome::AlreadySatisfied if bug.commit.is_some() => vec![step(
59            format!("speccoder report --feature {}", bug.unit),
60            "kept as a regression test: the evidence",
61        )],
62        Outcome::AlreadySatisfied => vec![
63            step(
64                format!("speccoder bug keep {key} --reason \"kept as a regression test\""),
65                "keep the reproduction as a regression test: committed, the task attested",
66            ),
67            step(
68                format!(
69                    "speccoder task skip {} --feature {} --reason \"already fixed\"",
70                    bug.task, bug.unit
71                ),
72                "drop it",
73            ),
74        ],
75        Outcome::AwaitingConfirmation => vec![
76            step(
77                format!(
78                    "speccoder bug confirm {key} --digest {}",
79                    bug.confirm_digest.clone().unwrap_or_default()
80                ),
81                "the reproductions show the bug: fix it",
82            ),
83            step(
84                format!("speccoder bug confirm {key} --reject --reason \"…\""),
85                "they do not: the author tries again, told why",
86            ),
87        ],
88        Outcome::Recorded => vec![step(
89            format!("speccoder bug run --task {key}"),
90            "reproduce it",
91        )],
92        Outcome::Fixed | Outcome::Failed => vec![step(
93            format!("speccoder report --feature {}", bug.unit),
94            "the evidence",
95        )],
96    }
97}
98
99/// The hand-off for `bug` stopping in `outcome`: tries, per-statement
100/// classes, what was and was not reproduced, cost, next steps.
101#[must_use]
102pub fn compose(
103    bug: &BugReport,
104    outcome: Outcome,
105    elapsed_ms: u64,
106    cost: Option<serde_json::Value>,
107    claim: Option<String>,
108) -> Handoff {
109    let mut statements = Statements::default();
110    for e in bug.expected_ids() {
111        // A discarded (or rejected) try is evidence of nothing.
112        let classes: Vec<RedClass> = bug
113            .tries
114            .iter()
115            .filter(|t| t.discarded.is_none())
116            .filter_map(|t| {
117                t.per_statement
118                    .iter()
119                    .find(|s| s.statement == e)
120                    .map(|s| s.class)
121            })
122            .collect();
123        if classes.contains(&RedClass::Assertion) {
124            statements.reproduced.push(e);
125        } else if !classes.is_empty() && classes.iter().all(|c| *c == RedClass::Passed) {
126            statements.satisfied.push(e);
127        } else {
128            statements.not_reproduced.push(e);
129        }
130    }
131    let mut tries = bug.tries.clone();
132    for t in &mut tries {
133        for s in &mut t.per_statement {
134            s.excerpt = s.excerpt.as_ref().map(|x| cut(x, 20));
135        }
136    }
137    Handoff {
138        outcome,
139        tries,
140        statements,
141        cost,
142        elapsed_ms,
143        next: next(bug, outcome),
144        claim: claim.filter(|c| !c.trim().is_empty()).map(|text| Claim {
145            text,
146            by: "author".to_owned(),
147        }),
148    }
149}
150
151/// The first `n` lines.
152#[must_use]
153pub fn cut(text: &str, n: usize) -> String {
154    text.lines()
155        .filter(|l| !l.trim().is_empty())
156        .take(n)
157        .collect::<Vec<_>>()
158        .join("\n")
159}