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 let why = tv
76 .and_then(|tv| {
77 serde_json::from_value::<speccoder_core::regression::RegressionReason>(
78 tv["regression"].clone(),
79 )
80 .ok()
81 })
82 .map(|r| speccoder_core::regression::wording(&r))
83 .or_else(|| task.regression.as_ref().map(|r| r.text.clone()))
84 .filter(|_| task.state == "regression_only");
85 match (phase(statements, "red"), phase(statements, "green")) {
87 (Some(red), Some(green)) => out.push(row(
88 "Acceptance",
89 "verified",
90 format!(
91 "red → green: {} failing before, {} passing after (spec-derived, frozen)",
92 names(&red["failedTests"]),
93 names(&green["passedTests"])
94 ),
95 )),
96 (None, Some(green)) => out.push(row(
97 "Acceptance",
98 "regression_only",
99 why.clone().map_or_else(
100 || {
101 format!(
102 "none red first — the checks alone ({} passing)",
103 names(&green["passedTests"])
104 )
105 },
106 |w| format!("none red first — {w}"),
107 ),
108 )),
109 _ => out.push(match why.clone() {
110 Some(w) => row(
111 "Acceptance",
112 "regression_only",
113 format!("none red first — {w}"),
114 ),
115 None => row("Acceptance", "ready", "not run".to_owned()),
116 }),
117 }
118 let collection = tv
120 .and_then(|tv| {
121 serde_json::from_value::<speccoder_core::collection::Collection>(
122 tv["collection"].clone(),
123 )
124 .ok()
125 })
126 .or_else(|| run.and_then(|r| r.collection.clone()));
127 let recorded = tv.is_some_and(|tv| tv["revision"].as_u64().is_some_and(|r| r >= 3));
128 if let Some(r) = collection_row(collection.as_ref(), recorded || run.is_some()) {
129 out.push(r);
130 }
131 out.push(match phase(statements, "held_out") {
132 Some(h) if h["result"] == "PASSED" => row(
133 "Held-out",
134 "verified",
135 format!(
136 "{} passed (judge only, never shown to the implementer)",
137 names(&h["passedTests"])
138 ),
139 ),
140 Some(h) => row(
141 "Held-out",
142 "failed",
143 format!("{} failed", names(&h["failedTests"])),
144 ),
145 None => row("Held-out", "ready", "not run".to_owned()),
146 });
147 out.push(match phase(statements, "baseline") {
148 Some(b) => {
149 let warned = names(&b["warnedTests"]);
150 row(
151 "Checks",
152 if warned == 0 {
153 "verified"
154 } else {
155 "regression_only"
156 },
157 if warned == 0 {
158 "baseline green, nothing regressed".to_owned()
159 } else {
160 format!(
161 "{warned} failing before the attempt (not blamed on it), nothing regressed"
162 )
163 },
164 )
165 }
166 None => row("Checks", "ready", "no record".to_owned()),
167 });
168 if let Some(tv) = tv {
169 let findings = names(&tv["integrity"]["findings"]);
170 out.push(row(
171 "Diff audit",
172 if findings == 0 { "verified" } else { "failed" },
173 if findings == 0 {
174 "no tests weakened, no skips, no config changes".to_owned()
175 } else {
176 format!("{findings} finding(s)")
177 },
178 ));
179 let monitor = tv["monitor"]["state"]
180 .as_str()
181 .or_else(|| tv["monitor"].as_str())
182 .unwrap_or("off");
183 out.push(row(
184 "Gaming monitor",
185 match monitor {
186 "clean" => "verified",
187 "suspected" => "needs_approval",
188 _ => "ready",
189 },
190 monitor.to_owned(),
191 ));
192 let mutation = &tv["mutation"];
193 out.push(if mutation.is_null() {
194 row("Mutation", "ready", "not run".to_owned())
195 } else {
196 let caught = mutation["caught"].as_u64().unwrap_or(0);
197 let missed = mutation["missed"].as_u64().unwrap_or(0);
198 row(
199 "Mutation",
200 if missed == 0 {
201 "verified"
202 } else {
203 "regression_only"
204 },
205 format!("{caught} caught, {missed} survived"),
206 )
207 });
208 let decisions: Vec<String> = tv["decisions"]
209 .as_array()
210 .into_iter()
211 .flatten()
212 .map(|d| {
213 format!(
214 "{} {} — {}{}",
215 d["verb"].as_str().unwrap_or("decided"),
216 d["grant"]
217 .as_str()
218 .or_else(|| d["tool"].as_str())
219 .unwrap_or_default(),
220 d["by"].as_str().unwrap_or("?"),
221 d["via"]
222 .as_str()
223 .map(|v| format!(" via {v}"))
224 .unwrap_or_default()
225 )
226 })
227 .collect();
228 out.push(row(
229 "Approvals",
230 if decisions.is_empty() {
231 "ready"
232 } else {
233 "verified"
234 },
235 if decisions.is_empty() {
236 "none needed".to_owned()
237 } else {
238 decisions.join("; ")
239 },
240 ));
241 let instructions: Vec<&str> = tv["instructions"]
242 .as_array()
243 .into_iter()
244 .flatten()
245 .filter_map(|i| i["path"].as_str())
246 .collect();
247 out.push(row(
248 "Instructions",
249 "ready",
250 if instructions.is_empty() {
251 "none".to_owned()
252 } else {
253 instructions.join(", ")
254 },
255 ));
256 if let Some(by) = tv["decider"]["by"].as_str() {
257 out.push(row(
258 "Decided by",
259 "attested",
260 format!(
261 "{by} — {}",
262 tv["decider"]["reason"].as_str().unwrap_or_default()
263 ),
264 ));
265 }
266 }
267 if let Some(st) = tv
269 .and_then(|tv| tv.get("strength"))
270 .filter(|v| !v.is_null())
271 {
272 let share = st["combined"]["share"].as_f64();
273 let rejected = st["permitted"]
274 .as_array()
275 .into_iter()
276 .flatten()
277 .any(|p| p["outcome"] == "rejected");
278 out.push(row(
279 "Strength",
280 match share {
281 _ if rejected => "needs_approval",
282 Some(x) if x >= 0.999 => "verified",
283 Some(_) => "regression_only",
284 None => "ready",
285 },
286 task.strength.clone().unwrap_or_else(|| {
287 share.map_or_else(|| "not measured".to_owned(), |x| format!("{x:.2}"))
288 }),
289 ));
290 } else if let Some(st) = &task.strength {
291 out.push(row("Strength", "ready", st.clone()));
292 }
293 out.push(match task.attestation.as_deref() {
294 Some(a) if a.starts_with("signed") => row("Attestation", "verified", a.to_owned()),
295 Some(a) => row("Attestation", "regression_only", a.to_owned()),
296 None => row(
297 "Attestation",
298 "ready",
299 "none (not ticked by speccoder)".to_owned(),
300 ),
301 });
302 if let Some(a) = &task.last_attempt {
303 out.push(row(
304 "Last attempt",
305 a.outcome.as_str(),
306 format!(
307 "{} — {} ({})",
308 a.outcome.replace('_', " "),
309 a.reason,
310 a.reference
311 ),
312 ));
313 }
314 if let Some(r) = run {
315 for e in &r.earlier {
316 out.push(row(
317 "Earlier attempt",
318 "failed",
319 format!("{} — run {}", e.attempt_status.replace('_', " "), e.run_id),
320 ));
321 }
322 for note in r
324 .acceptance
325 .as_ref()
326 .and_then(|a| a["notes"].as_array())
327 .into_iter()
328 .flatten()
329 .filter_map(|n| n.as_str())
330 .filter(|n| !n.trim().is_empty())
331 {
332 out.push(row(
333 "Test author",
334 "ready",
335 format!("the test author's note (a model's words): \"{note}\""),
336 ));
337 }
338 if let Some(claim) = r.model_report.as_ref().and_then(|m| m["summary"].as_str()) {
339 out.push(row(
340 "Model says",
341 "ready",
342 format!("\"{claim}\" (a claim, not evidence)"),
343 ));
344 }
345 }
346 out
347}
348
349#[must_use]
353pub fn collection_row(
354 c: Option<&speccoder_core::collection::Collection>,
355 recorded: bool,
356) -> Option<Row> {
357 use speccoder_core::collection::FileState;
358 let row = |mark: &str, text: String| Row {
359 label: "Collection".to_owned(),
360 mark: mark.to_owned(),
361 text,
362 };
363 let Some(c) = c.filter(|c| !c.files.is_empty()) else {
364 return recorded.then(|| row("ready", "no test files changed".to_owned()));
365 };
366 let never: Vec<&speccoder_core::collection::FileCollection> = c.not_collected().collect();
367 let unknown = c.unknown().count();
368 let ran: Vec<_> = c
369 .files
370 .iter()
371 .filter(|f| f.state == FileState::Collected)
372 .collect();
373 let mut parts = Vec::new();
374 if !ran.is_empty() {
375 let tests: usize = ran.iter().map(|f| f.tests).sum();
376 parts.push(format!(
377 "{} test file{} collected ({tests} test{})",
378 ran.len(),
379 if ran.len() == 1 { "" } else { "s" },
380 if tests == 1 { "" } else { "s" }
381 ));
382 }
383 if let Some(first) = never.first() {
384 parts.push(format!(
385 "{} never ran: `{}`{}",
386 never.len(),
387 first.path,
388 first
389 .rule
390 .as_ref()
391 .map(|r| format!(" — {}", r.text))
392 .unwrap_or_default()
393 ));
394 }
395 if unknown > 0 {
396 parts.push(format!("{unknown} could not be told"));
397 }
398 let mark = if !never.is_empty() {
399 "failed"
400 } else if unknown > 0 {
401 "needs_approval"
402 } else {
403 "verified"
404 };
405 Some(row(mark, parts.join(" · ")))
406}
407
408pub fn account_of(
416 path: &std::path::Path,
417 run: &str,
418) -> Result<Vec<crate::substrate::AccountTurn>, String> {
419 let root = crate::format::at(path)
420 .detect(path)
421 .ok_or_else(|| "not a spec project".to_owned())?;
422 let state = crate::worktree::state_dir().ok_or_else(|| "no state directory".to_owned())?;
423 let plane = crate::worktree::project_state_dir(&state, &root);
424 crate::pipeline::tokio_runtime()?
425 .block_on(crate::substrate::account(&plane, run))
426 .map_err(|e| e.0)
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use serde_json::json;
433
434 fn task(state: &str) -> TaskStatus {
435 TaskStatus {
436 id: "T002".to_owned(),
437 title: "t".to_owned(),
438 phase: "P".to_owned(),
439 state: state.to_owned(),
440 drift: None,
441 blocked_by: Vec::new(),
442 commit: Some("abc".to_owned()),
443 run_id: None,
444 last_attempt: None,
445 set_aside: None,
446 bug: None,
447 regression: None,
448 note: None,
449 attestation: Some("signed ssh SHA256:x".to_owned()),
450 strength: None,
451 }
452 }
453
454 fn phase_stmt(name: &str, result: &str, passed: &[&str], failed: &[&str]) -> Value {
455 json!({ "predicateType": "https://in-toto.io/attestation/test-result/v0.1", "predicate": {
456 "result": result, "configuration": [{ "name": format!("speccoder:phase/{name}") }],
457 "passedTests": passed, "failedTests": failed, "warnedTests": [] } })
458 }
459
460 #[test]
461 fn rows_come_from_the_statements() {
462 let statements = vec![
463 phase_stmt("baseline", "PASSED", &["a"], &[]),
464 phase_stmt("red", "FAILED", &[], &["accept::login"]),
465 phase_stmt("green", "PASSED", &["accept::login", "a"], &[]),
466 json!({ "predicateType": "https://speccoder.dev/task-verification/v0", "predicate": {
467 "integrity": { "findings": [] }, "monitor": { "state": "clean" },
468 "mutation": { "caught": 26, "missed": 3 },
469 "decisions": [{ "verb": "approve", "grant": "run `cargo add x`", "by": "Ada", "via": "dashboard" }],
470 "instructions": [{ "path": "AGENTS.md" }] } }),
471 ];
472 let rows = rows(&task("verified"), &statements, None);
473 let text = |label: &str| {
474 rows.iter()
475 .find(|r| r.label == label)
476 .map(|r| r.text.clone())
477 .unwrap_or_default()
478 };
479 assert!(
480 text("Acceptance").contains("1 failing before, 2 passing after"),
481 "{rows:?}"
482 );
483 assert_eq!(text("Held-out"), "not run");
484 assert_eq!(text("Mutation"), "26 caught, 3 survived");
485 assert!(text("Approvals").contains("Ada via dashboard"));
486 assert_eq!(text("Instructions"), "AGENTS.md");
487 assert!(text("Attestation").starts_with("signed"));
488 }
489
490 #[test]
491 fn a_regression_only_tick_says_why_and_its_collection_is_a_row() {
492 let statements = vec![
493 phase_stmt("green", "PASSED", &["a"], &[]),
494 json!({ "predicateType": "https://speccoder.dev/task-verification/v0", "predicate": {
495 "revision": 3,
496 "regression": { "reason": "already_passing", "implemented_by": { "task": "T003", "commit": "d433d77" } },
497 "collection": { "files": [
498 { "path": "tests/greet.rs", "state": "collected", "decided_by": "junit", "tests": 2, "test_ids": ["greet::greet::a", "greet::greet::b"] },
499 { "path": "tests/unit/x.rs", "state": "not_collected", "decided_by": "static", "tests": 0, "test_ids": [],
500 "rule": { "toolchain": "rust", "text": "Cargo builds `tests/*.rs` and `tests/<dir>/main.rs` (and the modules they declare)", "source": "Cargo.toml (default)" } }
501 ] } } }),
502 ];
503 let rows = rows(&task("regression_only"), &statements, None);
504 let row = |label: &str| rows.iter().find(|r| r.label == label).cloned().unwrap();
505 assert_eq!(
506 row("Acceptance").text,
507 "none red first — already passing before the attempt — implemented by T003"
508 );
509 let c = row("Collection");
510 assert_eq!(c.mark, "failed");
511 assert_eq!(
512 c.text,
513 "1 test file collected (2 tests) · 1 never ran: `tests/unit/x.rs` — Cargo builds `tests/*.rs` and `tests/<dir>/main.rs` (and the modules they declare)"
514 );
515 assert_eq!(
517 collection_row(None, true).map(|r| r.text),
518 Some("no test files changed".to_owned())
519 );
520 assert_eq!(collection_row(None, false), None);
521 let mut legacy = task("regression_only");
523 legacy.regression = Some(speccoder_core::regression::Regression::of(
524 speccoder_core::regression::RegressionReason::Unrecorded,
525 ));
526 let rows = super::rows(&legacy, &[phase_stmt("green", "PASSED", &["a"], &[])], None);
527 assert!(
528 rows[0]
529 .text
530 .ends_with("why was not recorded (ticked by an earlier speccoder)")
531 );
532 }
533
534 #[test]
535 fn the_test_author_s_note_is_a_model_s_words_in_the_detail() {
536 let mut run = crate::pipeline::outcome::bare_report("001-x", "T002", "/w", "completed");
537 run.acceptance = Some(json!({ "notes": ["the spec has no example for an empty name"] }));
538 let rows = rows(&task("regression_only"), &[], Some(&run));
539 let note = rows
540 .iter()
541 .find(|r| r.label == "Test author")
542 .unwrap_or_else(|| panic!("{rows:?}"));
543 assert_eq!(
544 note.text,
545 "the test author's note (a model's words): \"the spec has no example for an empty name\""
546 );
547 }
548
549 #[test]
550 fn banners_never_blur() {
551 assert_eq!(banner("verified").1, "VERIFIED");
552 assert!(
553 banner("regression_only")
554 .1
555 .contains("nothing task-specific")
556 );
557 assert!(banner("attested").1.contains("not evidence"));
558 assert_ne!(banner("attested").0, banner("verified").0);
559 }
560}