speccoder_runtime/pipeline/
outcome.rs1use speccoder_core::scope::ScopeFinding;
5use speccoder_core::verify::Verdict;
6use speccoder_core::{CommandStatus, RunReport};
7
8use crate::progress;
9
10use super::{add_usage, monitor_summary};
11
12pub(crate) fn scope_of(report: &RunReport, title: &str) -> ScopeFinding {
15 let exempt: Vec<String> = report
16 .acceptance
17 .as_ref()
18 .and_then(|a| a["files"].as_array())
19 .into_iter()
20 .flatten()
21 .filter_map(|f| f.as_str().map(str::to_owned))
22 .collect();
23 let changed: Vec<String> = report.changes.iter().map(|(p, _)| p.clone()).collect();
24 let named = speccoder_core::scope::named_paths(title);
25 speccoder_core::scope::finding(&named, &changed, &exempt)
26}
27
28pub(crate) fn saved_outcome(
30 report: &RunReport,
31 attempt_status: &str,
32 suspected: bool,
33) -> (String, String) {
34 let outcome = match attempt_status {
35 _ if suspected => "suspected_gaming".to_owned(),
36 "completed" | "integrity" => "verification_failed".to_owned(),
37 other => other.to_owned(),
38 };
39 let reason = match &report.verdict {
40 _ if suspected => monitor_summary(report.monitor.as_ref()),
41 Some(Verdict::Failed { reasons }) => reasons.join("; "),
42 _ => report
43 .reason
44 .clone()
45 .unwrap_or_else(|| attempt_status.to_owned()),
46 };
47 (outcome, reason)
48}
49
50pub(crate) fn status_and_message(
52 report: &RunReport,
53 attempt_status: &str,
54 suspected: bool,
55) -> (CommandStatus, String) {
56 match (&report.verdict, attempt_status) {
57 (_, "over_strict") => (
58 CommandStatus::NeedsHuman,
59 format!(
60 "the checks passed, but {}; the attempt is held, not ticked — rule on it (`speccoder task rule {task} --tests-right --reason …`), regenerate the tests (`speccoder run --task {task} --fresh-acceptance`) or accept it (`speccoder task accept {task}`)",
61 report.reason.clone().unwrap_or_default(),
62 task = report.task
63 ),
64 ),
65 (_, "acceptance_weak") => (
66 CommandStatus::NeedsHuman,
67 format!(
68 "the checks passed, but the {}; the attempt is saved, not ticked",
69 report.reason.clone().unwrap_or_default()
70 ),
71 ),
72 _ if suspected => (
73 CommandStatus::NeedsHuman,
74 format!(
75 "the checks passed, but the gaming monitor suspects: {}; the attempt is saved, not ticked — review it, then `speccoder task accept` or run again",
76 monitor_summary(report.monitor.as_ref())
77 ),
78 ),
79 (_, "incomplete") => (
80 CommandStatus::NeedsHuman,
81 format!(
82 "attempt incomplete by the model's own account{}",
83 if report.changes.is_empty() {
84 "; no files changed"
85 } else {
86 ""
87 }
88 ),
89 ),
90 (Some(Verdict::Failed { reasons }), _) => (
91 CommandStatus::VerificationFailed,
92 format!("verdict: failed — {}", reasons.join("; ")),
93 ),
94 (Some(v), _) => (CommandStatus::Ok, format!("verdict: {}", verdict_word(v))),
95 (
96 None,
97 "blocked" | "needs_approval" | "budget_exhausted" | "denied" | "interrupted"
98 | "held_out" | "intent_changed" | "judge_changed" | "checkout_changed" | "git_changed",
99 ) => (
100 CommandStatus::NeedsHuman,
101 format!(
102 "attempt {attempt_status}: {}",
103 report
104 .reason
105 .clone()
106 .or_else(|| report
107 .pending
108 .as_ref()
109 .map(|p| p["reason"].as_str().unwrap_or_default().to_owned()))
110 .unwrap_or_default()
111 ),
112 ),
113 (None, _) => (
114 CommandStatus::RunFailed,
115 format!(
116 "attempt failed: {}",
117 report.reason.clone().unwrap_or_default()
118 ),
119 ),
120 }
121}
122
123pub(crate) fn verdict_word(v: &Verdict) -> String {
125 serde_json::to_value(v)
126 .ok()
127 .and_then(|v| v["verdict"].as_str().map(str::to_owned))
128 .unwrap_or_default()
129}
130
131pub(crate) fn finished_event(
133 task: &str,
134 report: &RunReport,
135 attempt_status: &str,
136 suspected: bool,
137 message: String,
138) -> progress::Event {
139 let total = add_usage(
140 report.usage.as_ref(),
141 report.acceptance.as_ref().and_then(|a| a.get("usage")),
142 );
143 let tokens = |k: &str| total.as_ref().and_then(|u| u[k].as_u64()).unwrap_or(0);
144 progress::Event::TaskFinished {
145 task: task.to_owned(),
146 outcome: if suspected {
149 "suspected_gaming".to_owned()
150 } else if attempt_status == "completed" {
151 report
152 .verdict
153 .as_ref()
154 .map_or_else(|| attempt_status.to_owned(), verdict_word)
155 } else {
156 attempt_status.to_owned()
157 },
158 message,
159 run: (!report.run_id.is_empty()).then(|| report.run_id.clone()),
160 input_tokens: tokens("input_tokens"),
161 output_tokens: tokens("output_tokens"),
162 cache_read_tokens: tokens("cache_read_tokens"),
163 cache_write_tokens: tokens("cache_write_tokens"),
164 usd: None,
165 }
166}
167
168pub(crate) fn bare_report(
170 feature: &str,
171 task: &str,
172 worktree: &str,
173 attempt_status: &str,
174) -> RunReport {
175 RunReport {
176 run_id: String::new(),
177 feature: feature.to_owned(),
178 task: task.to_owned(),
179 worktree: worktree.to_owned(),
180 attempt_status: attempt_status.to_owned(),
181 model_report: None,
182 reason: None,
183 pending: None,
184 tool_calls: Vec::new(),
185 changes: Vec::new(),
186 verification: None,
187 verdict: None,
188 usage: None,
189 integrity: Vec::new(),
190 ticked: false,
191 commit: None,
192 acceptance: None,
193 earlier: Vec::new(),
194 retry_stop: None,
195 saved_attempt: None,
196 set_aside: None,
197 notices: Vec::new(),
198 monitor: None,
199 mutation: None,
200 samples: Vec::new(),
201 scope: None,
202 attestation: None,
203 instructions: Vec::new(),
204 strength: None,
205 bug: None,
206 }
207}
208
209pub(crate) fn undemonstrated_why(acceptance: Option<&super::PreparedAcceptance>) -> String {
212 match acceptance {
213 None => "the spec has no acceptance examples to test it against".to_owned(),
214 Some(a) => match (a.report["skipped"].as_str(), a.report["note"].as_str()) {
215 (Some(s), _) => format!("no acceptance tests ({s})"),
216 (None, Some(note)) => match a.report["excerpt"].as_str().and_then(|e| e.lines().next())
217 {
218 Some(first) => format!("{note}: {first}"),
219 None => note.to_owned(),
220 },
221 (None, None) => "its acceptance tests already pass before any change".to_owned(),
222 },
223 }
224}