use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Tier {
Fast,
Normal,
Deep,
}
impl Tier {
pub fn order(self) -> u8 {
match self {
Self::Fast => 0,
Self::Normal => 1,
Self::Deep => 2,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Fast => "fast",
Self::Normal => "normal",
Self::Deep => "deep",
}
}
pub fn from_str_opt(s: &str) -> Option<Self> {
match s {
"fast" => Some(Self::Fast),
"normal" => Some(Self::Normal),
"deep" => Some(Self::Deep),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FitnessKind {
Atomic,
Holistic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisMode {
Static,
Dynamic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionScope {
Local,
Ci,
Staging,
ProdObservation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Gate {
Hard,
Soft,
Advisory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Stability {
Deterministic,
Noisy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceType {
Command,
Test,
Probe,
Sarif,
ManualAttestation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Confidence {
High,
Medium,
Low,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultState {
Pass,
Fail,
Unknown,
Skipped,
Waived,
}
impl ResultState {
pub fn as_str(&self) -> &'static str {
match self {
Self::Pass => "pass",
Self::Fail => "fail",
Self::Unknown => "unknown",
Self::Skipped => "skipped",
Self::Waived => "waived",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Waiver {
pub reason: String,
#[serde(default)]
pub owner: String,
#[serde(default)]
pub tracking_issue: Option<i64>,
#[serde(default)]
pub expires_at: Option<NaiveDate>,
}
impl Waiver {
pub fn is_active(&self, today: Option<NaiveDate>) -> bool {
let reference = today.unwrap_or_else(|| chrono::Utc::now().date_naive());
match self.expires_at {
None => true,
Some(expiry) => expiry >= reference,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Metric {
pub name: String,
pub command: String,
pub pattern: String,
pub hard_gate: bool,
pub tier: Tier,
pub description: String,
pub kind: FitnessKind,
pub analysis: AnalysisMode,
pub execution_scope: ExecutionScope,
pub gate: Gate,
pub stability: Stability,
pub evidence_type: EvidenceType,
pub scope: Vec<String>,
pub run_when_changed: Vec<String>,
pub timeout_seconds: Option<u64>,
pub owner: String,
pub confidence: Confidence,
pub waiver: Option<Waiver>,
}
impl Metric {
pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
Self {
name: name.into(),
command: command.into(),
pattern: String::new(),
hard_gate: false,
tier: Tier::Normal,
description: String::new(),
kind: FitnessKind::Atomic,
analysis: AnalysisMode::Static,
execution_scope: ExecutionScope::Local,
gate: Gate::Soft,
stability: Stability::Deterministic,
evidence_type: EvidenceType::Command,
scope: Vec::new(),
run_when_changed: Vec::new(),
timeout_seconds: None,
owner: String::new(),
confidence: Confidence::Unknown,
waiver: None,
}
}
pub fn with_hard_gate(mut self, hard_gate: bool) -> Self {
self.hard_gate = hard_gate;
if hard_gate {
self.gate = Gate::Hard;
} else if self.gate == Gate::Hard {
self.gate = Gate::Soft;
}
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Dimension {
pub name: String,
pub weight: i32,
pub threshold_pass: i32,
pub threshold_warn: i32,
pub metrics: Vec<Metric>,
pub source_file: String,
}
impl Dimension {
pub fn new(name: impl Into<String>, weight: i32) -> Self {
Self {
name: name.into(),
weight,
threshold_pass: 90,
threshold_warn: 80,
metrics: Vec::new(),
source_file: String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MetricResult {
pub metric_name: String,
pub passed: bool,
pub output: String,
pub tier: Tier,
pub hard_gate: bool,
pub duration_ms: f64,
pub state: ResultState,
}
impl MetricResult {
pub fn new(
metric_name: impl Into<String>,
passed: bool,
output: impl Into<String>,
tier: Tier,
) -> Self {
let state = if passed {
ResultState::Pass
} else {
ResultState::Fail
};
Self {
metric_name: metric_name.into(),
passed,
output: output.into(),
tier,
hard_gate: false,
duration_ms: 0.0,
state,
}
}
pub fn with_state(mut self, state: ResultState) -> Self {
self.state = state;
self
}
pub fn with_hard_gate(mut self, hard_gate: bool) -> Self {
self.hard_gate = hard_gate;
self
}
pub fn with_duration_ms(mut self, duration_ms: f64) -> Self {
self.duration_ms = duration_ms;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DimensionScore {
pub dimension: String,
pub weight: i32,
pub passed: usize,
pub total: usize,
pub score: f64,
pub hard_gate_failures: Vec<String>,
pub results: Vec<MetricResult>,
}
impl DimensionScore {
pub fn new(
dimension: impl Into<String>,
weight: i32,
passed: usize,
total: usize,
score: f64,
) -> Self {
Self {
dimension: dimension.into(),
weight,
passed,
total,
score,
hard_gate_failures: Vec::new(),
results: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FitnessReport {
pub dimensions: Vec<DimensionScore>,
pub final_score: f64,
pub hard_gate_blocked: bool,
pub score_blocked: bool,
}
impl Default for FitnessReport {
fn default() -> Self {
Self {
dimensions: Vec::new(),
final_score: 0.0,
hard_gate_blocked: false,
score_blocked: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tier_order() {
assert!(Tier::Fast.order() < Tier::Normal.order());
assert!(Tier::Normal.order() < Tier::Deep.order());
}
#[test]
fn test_tier_values() {
assert_eq!(Tier::Fast.as_str(), "fast");
assert_eq!(Tier::Normal.as_str(), "normal");
assert_eq!(Tier::Deep.as_str(), "deep");
}
#[test]
fn test_metric_defaults() {
let m = Metric::new("lint", "npm run lint");
assert_eq!(m.pattern, "");
assert!(!m.hard_gate);
assert_eq!(m.tier, Tier::Normal);
assert_eq!(m.kind, FitnessKind::Atomic);
assert_eq!(m.analysis, AnalysisMode::Static);
assert_eq!(m.execution_scope, ExecutionScope::Local);
assert_eq!(m.gate, Gate::Soft);
assert_eq!(m.stability, Stability::Deterministic);
assert_eq!(m.evidence_type, EvidenceType::Command);
assert!(m.scope.is_empty());
assert!(m.run_when_changed.is_empty());
assert_eq!(m.timeout_seconds, None);
assert_eq!(m.owner, "");
assert_eq!(m.confidence, Confidence::Unknown);
assert!(m.waiver.is_none());
}
#[test]
fn test_metric_hard_gate_sets_default_gate() {
let m = Metric::new("lint", "npm run lint").with_hard_gate(true);
assert_eq!(m.gate, Gate::Hard);
}
#[test]
fn test_metric_disabling_hard_gate_resets_hard_gate_default() {
let m = Metric::new("lint", "npm run lint")
.with_hard_gate(true)
.with_hard_gate(false);
assert!(!m.hard_gate);
assert_eq!(m.gate, Gate::Soft);
}
#[test]
fn test_metric_disabling_hard_gate_preserves_non_hard_gate() {
let mut m = Metric::new("lint", "npm run lint");
m.gate = Gate::Advisory;
let m = m.with_hard_gate(false);
assert_eq!(m.gate, Gate::Advisory);
}
#[test]
fn test_dimension_defaults() {
let d = Dimension::new("security", 20);
assert_eq!(d.threshold_pass, 90);
assert_eq!(d.threshold_warn, 80);
assert!(d.metrics.is_empty());
assert_eq!(d.source_file, "");
}
#[test]
fn test_metric_result() {
let r = MetricResult::new("lint", true, "ok", Tier::Fast);
assert!(!r.hard_gate);
assert_eq!(r.duration_ms, 0.0);
assert_eq!(r.state, ResultState::Pass);
}
#[test]
fn test_metric_result_failed_defaults_state() {
let r = MetricResult::new("lint", false, "boom", Tier::Fast);
assert_eq!(r.state, ResultState::Fail);
}
#[test]
fn test_metric_result_explicit_state_preserved() {
let r = MetricResult::new("lint", false, "skipped", Tier::Fast)
.with_state(ResultState::Skipped);
assert_eq!(r.state, ResultState::Skipped);
}
#[test]
fn test_waiver_model() {
let waiver = Waiver {
reason: "legacy hotspot".to_string(),
owner: "platform".to_string(),
tracking_issue: Some(217),
expires_at: None,
};
assert_eq!(waiver.reason, "legacy hotspot");
assert_eq!(waiver.owner, "platform");
assert_eq!(waiver.tracking_issue, Some(217));
}
#[test]
fn test_dimension_score() {
let ds = DimensionScore::new("security", 20, 3, 4, 75.0);
assert!(ds.hard_gate_failures.is_empty());
assert!(ds.results.is_empty());
}
#[test]
fn test_fitness_report_defaults() {
let r = FitnessReport::default();
assert!(r.dimensions.is_empty());
assert_eq!(r.final_score, 0.0);
assert!(!r.hard_gate_blocked);
assert!(!r.score_blocked);
}
}