use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Verdict {
Pass,
Fail,
Skip,
Uncertain,
PendingReview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EvidenceProvenance {
Computational,
Inferential,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioResult {
pub scenario_name: String,
pub verdict: Verdict,
pub step_results: Vec<StepVerdict>,
pub evidence: Vec<Evidence>,
pub duration_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provenance: Option<EvidenceProvenance>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepVerdict {
pub step_text: String,
pub verdict: Verdict,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Evidence {
TestOutput {
test_name: String,
stdout: String,
passed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
package: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
level: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
test_double: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
targets: Option<String>,
},
CodeSnippet {
file: String,
line: usize,
content: String,
},
AiAnalysis {
model: String,
confidence: f64,
reasoning: String,
},
PatternMatch {
pattern: String,
matched: bool,
locations: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Checkpoint {
pub spec_name: String,
pub timestamp: u64,
pub vcs_ref: Option<String>,
pub scenarios: HashMap<String, CheckpointEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointEntry {
pub verdict: Verdict,
pub vcs_ref: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiRequest {
pub spec_name: String,
pub scenario_name: String,
pub steps: Vec<String>,
pub code_paths: Vec<String>,
#[serde(default)]
pub contract_intent: String,
#[serde(default)]
pub contract_constraints: Vec<String>,
#[serde(default)]
pub change_paths: Vec<String>,
#[serde(default)]
pub prior_evidence: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiDecision {
pub model: String,
pub confidence: f64,
pub verdict: Verdict,
pub reasoning: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationSummary {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub uncertain: usize,
#[serde(default)]
pub pending_review: usize,
}
impl VerificationSummary {
pub fn pass_rate(&self) -> f64 {
if self.total == 0 {
return 0.0;
}
self.passed as f64 / self.total as f64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationReport {
pub spec_name: String,
pub results: Vec<ScenarioResult>,
pub summary: VerificationSummary,
}
impl VerificationReport {
pub fn from_results(spec_name: String, results: Vec<ScenarioResult>) -> Self {
let total = results.len();
let passed = results
.iter()
.filter(|r| r.verdict == Verdict::Pass)
.count();
let failed = results
.iter()
.filter(|r| r.verdict == Verdict::Fail)
.count();
let skipped = results
.iter()
.filter(|r| r.verdict == Verdict::Skip)
.count();
let uncertain = results
.iter()
.filter(|r| r.verdict == Verdict::Uncertain)
.count();
let pending_review = results
.iter()
.filter(|r| r.verdict == Verdict::PendingReview)
.count();
Self {
spec_name,
results,
summary: VerificationSummary {
total,
passed,
failed,
skipped,
uncertain,
pending_review,
},
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_json_provenance_additive_only() {
let none = ScenarioResult {
scenario_name: "s".into(),
verdict: Verdict::Pass,
step_results: vec![],
evidence: vec![],
duration_ms: 0,
provenance: None,
};
let json = serde_json::to_string(&none).unwrap();
assert!(
!json.contains("provenance"),
"None must skip the key: {json}"
);
let some = ScenarioResult {
provenance: Some(EvidenceProvenance::Computational),
..none
};
let json = serde_json::to_string(&some).unwrap();
assert!(json.contains("\"provenance\":\"computational\""));
}
#[test]
fn test_rule_events_additive_empty_by_default() {
use crate::spec_core::{BehaviorRule, RuleKey, RuleScope, Span};
let rule = BehaviorRule {
key: RuleKey {
scope: RuleScope::Task("t".into()),
id: "r".into(),
},
name: "r".into(),
scenario_names: vec![],
events: vec![],
span: Span::line(1),
};
let json = serde_json::to_string(&rule).unwrap();
assert!(
!json.contains("\"events\""),
"empty events must skip key: {json}"
);
}
#[test]
fn test_rule_event_roundtrips() {
use crate::spec_core::{RuleEvent, RuleEventKind};
let ev = RuleEvent {
kind: RuleEventKind::Promoted,
note: "from task-foo".into(),
};
let json = serde_json::to_string(&ev).unwrap();
let back: RuleEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back, ev);
assert!(json.contains("\"promoted\""));
}
}