use serde::{Deserialize, Serialize};
mod evidence;
pub use evidence::{
BreakingChange, CliSubGate, CommitMatch, ComplexityGate, ContractDiffGate, DeterminismGate,
DiffCoverageGate, Evidence, EvidenceSource, GateMeta, GateStatus, HighComplexityFile,
MutationGate, MutationSurvivor, SchemaSubGate, ScopeCoverage, SemverSubGate, SupplyChainGate,
UncoveredHunk, Vulnerability,
};
pub const COCKPIT_SCHEMA_VERSION: u32 = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CockpitReceipt {
pub schema_version: u32,
pub mode: String,
pub generated_at_ms: u64,
pub base_ref: String,
pub head_ref: String,
pub change_surface: ChangeSurface,
pub composition: Composition,
pub code_health: CodeHealth,
pub risk: Risk,
pub contracts: Contracts,
pub evidence: Evidence,
pub review_plan: Vec<ReviewItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trend: Option<TrendComparison>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeSurface {
pub commits: usize,
pub files_changed: usize,
pub insertions: usize,
pub deletions: usize,
pub net_lines: i64,
pub churn_velocity: f64,
pub change_concentration: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Composition {
pub code_pct: f64,
pub test_pct: f64,
pub docs_pct: f64,
pub config_pct: f64,
pub test_ratio: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeHealth {
pub score: u32,
pub grade: String,
pub large_files_touched: usize,
pub avg_file_size: usize,
pub complexity_indicator: ComplexityIndicator,
pub warnings: Vec<HealthWarning>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComplexityIndicator {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthWarning {
pub path: String,
pub warning_type: WarningType,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WarningType {
LargeFile,
HighChurn,
LowTestCoverage,
ComplexChange,
BusFactor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Risk {
pub hotspots_touched: Vec<String>,
pub bus_factor_warnings: Vec<String>,
pub level: RiskLevel,
pub score: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RiskLevel {
Low,
Medium,
High,
Critical,
}
impl std::fmt::Display for RiskLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RiskLevel::Low => write!(f, "low"),
RiskLevel::Medium => write!(f, "medium"),
RiskLevel::High => write!(f, "high"),
RiskLevel::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contracts {
pub api_changed: bool,
pub cli_changed: bool,
pub schema_changed: bool,
pub breaking_indicators: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewItem {
pub path: String,
pub reason: String,
pub priority: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lines_changed: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TrendComparison {
pub baseline_available: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_generated_at_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub health: Option<TrendMetric>,
#[serde(skip_serializing_if = "Option::is_none")]
pub risk: Option<TrendMetric>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity: Option<TrendIndicator>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrendMetric {
pub current: f64,
pub previous: f64,
pub delta: f64,
pub delta_pct: f64,
pub direction: TrendDirection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrendIndicator {
pub direction: TrendDirection,
pub summary: String,
pub files_increased: usize,
pub files_decreased: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_cyclomatic_delta: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_cognitive_delta: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrendDirection {
Improving,
Stable,
Degrading,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cockpit_receipt_serde_roundtrip() {
let receipt = CockpitReceipt {
schema_version: COCKPIT_SCHEMA_VERSION,
mode: "cockpit".to_string(),
generated_at_ms: 1000,
base_ref: "main".to_string(),
head_ref: "HEAD".to_string(),
change_surface: ChangeSurface {
commits: 1,
files_changed: 2,
insertions: 10,
deletions: 5,
net_lines: 5,
churn_velocity: 15.0,
change_concentration: 0.8,
},
composition: Composition {
code_pct: 70.0,
test_pct: 20.0,
docs_pct: 5.0,
config_pct: 5.0,
test_ratio: 0.29,
},
code_health: CodeHealth {
score: 85,
grade: "B".to_string(),
large_files_touched: 0,
avg_file_size: 100,
complexity_indicator: ComplexityIndicator::Low,
warnings: vec![],
},
risk: Risk {
hotspots_touched: vec![],
bus_factor_warnings: vec![],
level: RiskLevel::Low,
score: 10,
},
contracts: Contracts {
api_changed: false,
cli_changed: false,
schema_changed: false,
breaking_indicators: 0,
},
evidence: Evidence {
overall_status: GateStatus::Pass,
mutation: MutationGate {
meta: GateMeta {
status: GateStatus::Pass,
source: EvidenceSource::RanLocal,
commit_match: CommitMatch::Exact,
scope: ScopeCoverage {
relevant: vec![],
tested: vec![],
ratio: 1.0,
lines_relevant: None,
lines_tested: None,
},
evidence_commit: None,
evidence_generated_at_ms: None,
},
survivors: vec![],
killed: 0,
timeout: 0,
unviable: 0,
},
diff_coverage: None,
contracts: None,
supply_chain: None,
determinism: None,
complexity: None,
},
review_plan: vec![],
trend: None,
};
let json = serde_json::to_string(&receipt).expect("serialize");
let back: CockpitReceipt = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.schema_version, COCKPIT_SCHEMA_VERSION);
assert_eq!(back.mode, "cockpit");
}
#[test]
fn gate_status_serde() {
let json = serde_json::to_string(&GateStatus::Pass).unwrap();
assert_eq!(json, "\"pass\"");
let back: GateStatus = serde_json::from_str(&json).unwrap();
assert_eq!(back, GateStatus::Pass);
}
#[test]
fn trend_direction_serde() {
let json = serde_json::to_string(&TrendDirection::Improving).unwrap();
assert_eq!(json, "\"improving\"");
let back: TrendDirection = serde_json::from_str(&json).unwrap();
assert_eq!(back, TrendDirection::Improving);
}
#[test]
fn risk_level_display() {
assert_eq!(RiskLevel::Low.to_string(), "low");
assert_eq!(RiskLevel::Critical.to_string(), "critical");
}
}