1use serde_json::Value;
8use speccoder_core::{RunReport, TaskStatus};
9
10pub use speccoder_core::evidence::Row;
11
12#[must_use]
14pub fn banner(state: &str) -> (&'static str, &'static str) {
15 match state {
16 "verified" => ("verified", "VERIFIED"),
17 "regression_only" => (
18 "regression_only",
19 "REGRESSION ONLY — nothing task-specific was shown",
20 ),
21 "attested" => ("attested", "ATTESTED — a person's word, not evidence"),
22 "ticked_without_evidence" => (
23 "ticked_without_evidence",
24 "TICKED WITHOUT SPECCODER EVIDENCE",
25 ),
26 "evidence_without_tick" => (
27 "evidence_without_tick",
28 "COMMITTED — its local tick is missing",
29 ),
30 "skipped" => ("skipped", "SKIPPED"),
31 "ready" => ("ready", "READY"),
32 _ => ("blocked", "BLOCKED"),
33 }
34}
35
36#[must_use]
38pub fn statements(repo: &std::path::Path, commit: &str) -> Vec<Value> {
39 let Some(note) = crate::attest::notes::read(repo, commit) else {
40 return Vec::new();
41 };
42 crate::attest::dsse::decode(¬e)
43 .0
44 .iter()
45 .filter_map(crate::attest::dsse::Envelope::statement)
46 .collect()
47}
48
49fn phase<'a>(statements: &'a [Value], name: &str) -> Option<&'a Value> {
50 statements.iter().find_map(|s| {
51 let p = &s["predicate"];
52 (p["configuration"][0]["name"] == format!("speccoder:phase/{name}")).then_some(p)
53 })
54}
55
56fn names(v: &Value) -> usize {
57 v.as_array().map_or(0, Vec::len)
58}
59
60#[must_use]
63pub fn rows(task: &TaskStatus, statements: &[Value], run: Option<&RunReport>) -> Vec<Row> {
64 let mut out = Vec::new();
65 let row = |label: &str, mark: &str, text: String| Row {
66 label: label.to_owned(),
67 mark: mark.to_owned(),
68 text,
69 };
70 let tv = statements
71 .iter()
72 .find(|s| s["predicateType"] == "https://speccoder.dev/task-verification/v0")
73 .map(|s| &s["predicate"]);
74 match (phase(statements, "red"), phase(statements, "green")) {
76 (Some(red), Some(green)) => out.push(row(
77 "Acceptance",
78 "verified",
79 format!(
80 "red → green: {} failing before, {} passing after (spec-derived, frozen)",
81 names(&red["failedTests"]),
82 names(&green["passedTests"])
83 ),
84 )),
85 (None, Some(green)) => out.push(row(
86 "Acceptance",
87 "regression_only",
88 format!(
89 "none red first — the checks alone ({} passing)",
90 names(&green["passedTests"])
91 ),
92 )),
93 _ => out.push(row("Acceptance", "ready", "not run".to_owned())),
94 }
95 out.push(match phase(statements, "held_out") {
96 Some(h) if h["result"] == "PASSED" => row(
97 "Held-out",
98 "verified",
99 format!(
100 "{} passed (judge only, never shown to the implementer)",
101 names(&h["passedTests"])
102 ),
103 ),
104 Some(h) => row(
105 "Held-out",
106 "failed",
107 format!("{} failed", names(&h["failedTests"])),
108 ),
109 None => row("Held-out", "ready", "not run".to_owned()),
110 });
111 out.push(match phase(statements, "baseline") {
112 Some(b) => {
113 let warned = names(&b["warnedTests"]);
114 row(
115 "Checks",
116 if warned == 0 {
117 "verified"
118 } else {
119 "regression_only"
120 },
121 if warned == 0 {
122 "baseline green, nothing regressed".to_owned()
123 } else {
124 format!(
125 "{warned} failing before the attempt (not blamed on it), nothing regressed"
126 )
127 },
128 )
129 }
130 None => row("Checks", "ready", "no record".to_owned()),
131 });
132 if let Some(tv) = tv {
133 let findings = names(&tv["integrity"]["findings"]);
134 out.push(row(
135 "Diff audit",
136 if findings == 0 { "verified" } else { "failed" },
137 if findings == 0 {
138 "no tests weakened, no skips, no config changes".to_owned()
139 } else {
140 format!("{findings} finding(s)")
141 },
142 ));
143 let monitor = tv["monitor"]["state"]
144 .as_str()
145 .or_else(|| tv["monitor"].as_str())
146 .unwrap_or("off");
147 out.push(row(
148 "Gaming monitor",
149 match monitor {
150 "clean" => "verified",
151 "suspected" => "needs_approval",
152 _ => "ready",
153 },
154 monitor.to_owned(),
155 ));
156 let mutation = &tv["mutation"];
157 out.push(if mutation.is_null() {
158 row("Mutation", "ready", "not run".to_owned())
159 } else {
160 let caught = mutation["caught"].as_u64().unwrap_or(0);
161 let missed = mutation["missed"].as_u64().unwrap_or(0);
162 row(
163 "Mutation",
164 if missed == 0 {
165 "verified"
166 } else {
167 "regression_only"
168 },
169 format!("{caught} caught, {missed} survived"),
170 )
171 });
172 let decisions: Vec<String> = tv["decisions"]
173 .as_array()
174 .into_iter()
175 .flatten()
176 .map(|d| {
177 format!(
178 "{} {} — {}{}",
179 d["verb"].as_str().unwrap_or("decided"),
180 d["grant"]
181 .as_str()
182 .or_else(|| d["tool"].as_str())
183 .unwrap_or_default(),
184 d["by"].as_str().unwrap_or("?"),
185 d["via"]
186 .as_str()
187 .map(|v| format!(" via {v}"))
188 .unwrap_or_default()
189 )
190 })
191 .collect();
192 out.push(row(
193 "Approvals",
194 if decisions.is_empty() {
195 "ready"
196 } else {
197 "verified"
198 },
199 if decisions.is_empty() {
200 "none needed".to_owned()
201 } else {
202 decisions.join("; ")
203 },
204 ));
205 let instructions: Vec<&str> = tv["instructions"]
206 .as_array()
207 .into_iter()
208 .flatten()
209 .filter_map(|i| i["path"].as_str())
210 .collect();
211 out.push(row(
212 "Instructions",
213 "ready",
214 if instructions.is_empty() {
215 "none".to_owned()
216 } else {
217 instructions.join(", ")
218 },
219 ));
220 if let Some(by) = tv["decider"]["by"].as_str() {
221 out.push(row(
222 "Decided by",
223 "attested",
224 format!(
225 "{by} — {}",
226 tv["decider"]["reason"].as_str().unwrap_or_default()
227 ),
228 ));
229 }
230 }
231 if let Some(st) = tv
233 .and_then(|tv| tv.get("strength"))
234 .filter(|v| !v.is_null())
235 {
236 let share = st["combined"]["share"].as_f64();
237 let rejected = st["permitted"]
238 .as_array()
239 .into_iter()
240 .flatten()
241 .any(|p| p["outcome"] == "rejected");
242 out.push(row(
243 "Strength",
244 match share {
245 _ if rejected => "needs_approval",
246 Some(x) if x >= 0.999 => "verified",
247 Some(_) => "regression_only",
248 None => "ready",
249 },
250 task.strength.clone().unwrap_or_else(|| {
251 share.map_or_else(|| "not measured".to_owned(), |x| format!("{x:.2}"))
252 }),
253 ));
254 } else if let Some(st) = &task.strength {
255 out.push(row("Strength", "ready", st.clone()));
256 }
257 out.push(match task.attestation.as_deref() {
258 Some(a) if a.starts_with("signed") => row("Attestation", "verified", a.to_owned()),
259 Some(a) => row("Attestation", "regression_only", a.to_owned()),
260 None => row(
261 "Attestation",
262 "ready",
263 "none (not ticked by speccoder)".to_owned(),
264 ),
265 });
266 if let Some(a) = &task.last_attempt {
267 out.push(row(
268 "Last attempt",
269 a.outcome.as_str(),
270 format!(
271 "{} — {} ({})",
272 a.outcome.replace('_', " "),
273 a.reason,
274 a.reference
275 ),
276 ));
277 }
278 if let Some(r) = run {
279 for e in &r.earlier {
280 out.push(row(
281 "Earlier attempt",
282 "failed",
283 format!("{} — run {}", e.attempt_status.replace('_', " "), e.run_id),
284 ));
285 }
286 if let Some(claim) = r.model_report.as_ref().and_then(|m| m["summary"].as_str()) {
287 out.push(row(
288 "Model says",
289 "ready",
290 format!("\"{claim}\" (a claim, not evidence)"),
291 ));
292 }
293 }
294 out
295}
296
297#[cfg(feature = "engine")]
305pub fn account_of(
306 path: &std::path::Path,
307 run: &str,
308) -> Result<Vec<crate::substrate::AccountTurn>, String> {
309 let root = crate::format::at(path)
310 .detect(path)
311 .ok_or_else(|| "not a spec project".to_owned())?;
312 let state = crate::worktree::state_dir().ok_or_else(|| "no state directory".to_owned())?;
313 let plane = crate::worktree::project_state_dir(&state, &root);
314 crate::pipeline::tokio_runtime()?
315 .block_on(crate::substrate::account(&plane, run))
316 .map_err(|e| e.0)
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use serde_json::json;
323
324 fn task(state: &str) -> TaskStatus {
325 TaskStatus {
326 id: "T002".to_owned(),
327 title: "t".to_owned(),
328 phase: "P".to_owned(),
329 state: state.to_owned(),
330 drift: None,
331 blocked_by: Vec::new(),
332 commit: Some("abc".to_owned()),
333 run_id: None,
334 last_attempt: None,
335 set_aside: None,
336 bug: None,
337 note: None,
338 attestation: Some("signed ssh SHA256:x".to_owned()),
339 strength: None,
340 }
341 }
342
343 fn phase_stmt(name: &str, result: &str, passed: &[&str], failed: &[&str]) -> Value {
344 json!({ "predicateType": "https://in-toto.io/attestation/test-result/v0.1", "predicate": {
345 "result": result, "configuration": [{ "name": format!("speccoder:phase/{name}") }],
346 "passedTests": passed, "failedTests": failed, "warnedTests": [] } })
347 }
348
349 #[test]
350 fn rows_come_from_the_statements() {
351 let statements = vec![
352 phase_stmt("baseline", "PASSED", &["a"], &[]),
353 phase_stmt("red", "FAILED", &[], &["accept::login"]),
354 phase_stmt("green", "PASSED", &["accept::login", "a"], &[]),
355 json!({ "predicateType": "https://speccoder.dev/task-verification/v0", "predicate": {
356 "integrity": { "findings": [] }, "monitor": { "state": "clean" },
357 "mutation": { "caught": 26, "missed": 3 },
358 "decisions": [{ "verb": "approve", "grant": "run `cargo add x`", "by": "Ada", "via": "dashboard" }],
359 "instructions": [{ "path": "AGENTS.md" }] } }),
360 ];
361 let rows = rows(&task("verified"), &statements, None);
362 let text = |label: &str| {
363 rows.iter()
364 .find(|r| r.label == label)
365 .map(|r| r.text.clone())
366 .unwrap_or_default()
367 };
368 assert!(
369 text("Acceptance").contains("1 failing before, 2 passing after"),
370 "{rows:?}"
371 );
372 assert_eq!(text("Held-out"), "not run");
373 assert_eq!(text("Mutation"), "26 caught, 3 survived");
374 assert!(text("Approvals").contains("Ada via dashboard"));
375 assert_eq!(text("Instructions"), "AGENTS.md");
376 assert!(text("Attestation").starts_with("signed"));
377 }
378
379 #[test]
380 fn banners_never_blur() {
381 assert_eq!(banner("verified").1, "VERIFIED");
382 assert!(
383 banner("regression_only")
384 .1
385 .contains("nothing task-specific")
386 );
387 assert!(banner("attested").1.contains("not evidence"));
388 assert_ne!(banner("attested").0, banner("verified").0);
389 }
390}