use serde::{Deserialize, Serialize};
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 Evidence {
pub overall_status: GateStatus,
pub mutation: MutationGate,
#[serde(skip_serializing_if = "Option::is_none")]
pub diff_coverage: Option<DiffCoverageGate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contracts: Option<ContractDiffGate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supply_chain: Option<SupplyChainGate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub determinism: Option<DeterminismGate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity: Option<ComplexityGate>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GateStatus {
Pass,
Warn,
Fail,
Skipped,
Pending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceSource {
CiArtifact,
Cached,
RanLocal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CommitMatch {
Exact,
Partial,
Stale,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateMeta {
pub status: GateStatus,
pub source: EvidenceSource,
pub commit_match: CommitMatch,
pub scope: ScopeCoverage,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence_generated_at_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopeCoverage {
pub relevant: Vec<String>,
pub tested: Vec<String>,
pub ratio: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub lines_relevant: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lines_tested: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationGate {
#[serde(flatten)]
pub meta: GateMeta,
pub survivors: Vec<MutationSurvivor>,
pub killed: usize,
pub timeout: usize,
pub unviable: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationSurvivor {
pub file: String,
pub line: usize,
pub mutation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffCoverageGate {
#[serde(flatten)]
pub meta: GateMeta,
pub lines_added: usize,
pub lines_covered: usize,
pub coverage_pct: f64,
pub uncovered_hunks: Vec<UncoveredHunk>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UncoveredHunk {
pub file: String,
pub start_line: usize,
pub end_line: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractDiffGate {
#[serde(flatten)]
pub meta: GateMeta,
#[serde(skip_serializing_if = "Option::is_none")]
pub semver: Option<SemverSubGate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cli: Option<CliSubGate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<SchemaSubGate>,
pub failures: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemverSubGate {
pub status: GateStatus,
pub breaking_changes: Vec<BreakingChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BreakingChange {
pub kind: String,
pub path: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliSubGate {
pub status: GateStatus,
pub diff_summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaSubGate {
pub status: GateStatus,
pub diff_summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupplyChainGate {
#[serde(flatten)]
pub meta: GateMeta,
pub vulnerabilities: Vec<Vulnerability>,
pub denied: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub advisory_db_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
pub id: String,
pub package: String,
pub severity: String,
pub title: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeterminismGate {
#[serde(flatten)]
pub meta: GateMeta,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actual_hash: Option<String>,
pub algo: String,
pub differences: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityGate {
#[serde(flatten)]
pub meta: GateMeta,
pub files_analyzed: usize,
pub high_complexity_files: Vec<HighComplexityFile>,
pub avg_cyclomatic: f64,
pub max_cyclomatic: u32,
pub threshold_exceeded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HighComplexityFile {
pub path: String,
pub cyclomatic: u32,
pub function_count: usize,
pub max_function_length: usize,
}
#[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");
}
}