main-spex 0.23.0

AI-first development framework that turns specifications into deterministic, verifiable code
use strum::IntoEnumIterator;

use crate::spec::{AcId, AcceptanceCriterion, ContractLevel, CoverageLayer, Spec};

use super::annotation_index::AnnotationIndex;

#[cfg(test)]
#[path = "spec_coverage_test.rs"]
mod spec_coverage_test;

pub struct CriterionCoverage {
    pub id: AcId,
    pub text: String,
    pub is_done: bool,
    pub is_started: bool,
    pub missing: Vec<CoverageLayer>,
}

impl CriterionCoverage {
    fn new(spec: &Spec, criterion: &AcceptanceCriterion, index: &AnnotationIndex) -> Self {
        let required = |layer| match layer {
            CoverageLayer::Contract => spec.ac_contract_required(&criterion.id),
            _ => !spec.ac_ignored(&criterion.id, layer),
        };
        Self {
            id: criterion.id.clone(),
            text: criterion.statement.text().to_string(),
            is_done: criterion.is_done,
            is_started: index.has_ac(&spec.id, &criterion.id, CoverageLayer::Impl),
            missing: CoverageLayer::iter()
                .filter(|&layer| required(layer) && !index.has_ac(&spec.id, &criterion.id, layer))
                .collect(),
        }
    }

    pub fn is_missing(&self, layer: CoverageLayer) -> bool {
        self.missing.contains(&layer)
    }

    pub fn is_implemented(&self) -> bool {
        !self.is_missing(CoverageLayer::Impl)
    }

    pub fn is_covered(&self) -> bool {
        self.missing.is_empty()
    }

    pub fn is_unstarted(&self) -> bool {
        !self.is_done && !self.is_started
    }

    pub fn is_gap(&self) -> bool {
        !self.is_covered() && !self.is_unstarted()
    }

    pub fn is_pending(&self) -> bool {
        !self.is_covered() && self.is_unstarted()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpecHealth {
    Gap,
    Pending,
    Complete,
}

pub struct SpecCoverage<'a> {
    pub spec: &'a Spec,
    pub contract_missing: bool,
    pub criteria: Vec<CriterionCoverage>,
}

impl<'a> SpecCoverage<'a> {
    pub fn new(spec: &'a Spec, index: &AnnotationIndex) -> Self {
        if !spec.status.is_tracked() {
            return Self {
                spec,
                contract_missing: false,
                criteria: Vec::new(),
            };
        }
        Self {
            spec,
            contract_missing: is_spec_contract_missing(spec, index),
            criteria: spec
                .acceptance_criteria
                .iter()
                .map(|criterion| CriterionCoverage::new(spec, criterion, index))
                .collect(),
        }
    }

    pub fn is_tracked(&self) -> bool {
        self.spec.status.is_tracked()
    }

    pub fn implemented_count(&self) -> usize {
        self.criteria
            .iter()
            .filter(|criterion| criterion.is_implemented())
            .count()
    }

    pub fn is_fully_implemented(&self) -> bool {
        self.implemented_count() == self.criteria.len()
    }

    pub fn is_contract_awaited(&self) -> bool {
        !self.criteria.is_empty() && self.criteria.iter().all(CriterionCoverage::is_unstarted)
    }

    pub fn has_contract_gap(&self) -> bool {
        self.contract_missing && !self.is_contract_awaited()
    }

    pub fn has_contract_pending(&self) -> bool {
        self.contract_missing && self.is_contract_awaited()
    }

    pub fn has_gap(&self) -> bool {
        self.has_contract_gap() || self.criteria.iter().any(CriterionCoverage::is_gap)
    }

    pub fn has_pending(&self) -> bool {
        self.has_contract_pending() || self.criteria.iter().any(CriterionCoverage::is_pending)
    }

    pub fn health(&self) -> SpecHealth {
        if self.has_gap() {
            SpecHealth::Gap
        } else if self.has_pending() {
            SpecHealth::Pending
        } else {
            SpecHealth::Complete
        }
    }

    pub fn gapped_criteria(&self) -> Vec<&CriterionCoverage> {
        self.criteria
            .iter()
            .filter(|criterion| criterion.is_gap())
            .collect()
    }

    pub fn pending_criteria(&self) -> Vec<&CriterionCoverage> {
        self.criteria
            .iter()
            .filter(|criterion| criterion.is_pending())
            .collect()
    }
}

fn is_spec_contract_missing(spec: &Spec, index: &AnnotationIndex) -> bool {
    spec.contract == ContractLevel::Single
        && !spec.layer_ignored(CoverageLayer::Contract)
        && !index.has_bare(&spec.id, CoverageLayer::Contract)
}