use serde::{Deserialize, Serialize};
use crate::model::{Contradiction, HypothesisStanding, Priority, RequirementCandidate, Severity};
use crate::projection::{AcceptStance, ClaimWithStanding};
use crate::session::CoverageSchema;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DimensionStatus {
pub dimension: String,
pub required: bool,
pub best_standing: Option<HypothesisStanding>,
pub claims: Vec<String>,
pub satisfied: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoverageGap {
pub dimension: String,
pub suggested_question: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadinessReport {
pub ready: bool,
pub dimensions: Vec<DimensionStatus>,
pub gaps: Vec<CoverageGap>,
pub blockers: Vec<String>,
}
pub(crate) fn assess(
schema: &CoverageSchema,
claims: &[ClaimWithStanding],
contradictions: &[Contradiction],
stances: &[AcceptStance],
requirements: &[RequirementCandidate],
) -> ReadinessReport {
let mut dimensions = Vec::new();
let mut gaps = Vec::new();
let mut blockers = Vec::new();
for dim in &schema.dimensions {
let mut best: Option<HypothesisStanding> = None;
let mut dim_claims = Vec::new();
for cw in claims {
if cw.claim.dimensions.iter().any(|t| t == &dim.tag) {
dim_claims.push(cw.claim.id.clone());
best = Some(match best {
Some(b) if b.rank() >= cw.standing.rank() => b,
_ => cw.standing,
});
}
}
let satisfied = best.map(|b| b.is_withstood_or_better()).unwrap_or(false);
if dim.required && !satisfied {
let suggested_question = match best {
None => format!("What is the {} for this work?", dim.tag),
Some(_) => format!(
"The {} hypothesis isn't settled yet — can you confirm or refute it?",
dim.tag
),
};
gaps.push(CoverageGap {
dimension: dim.tag.clone(),
suggested_question,
});
blockers.push(format!(
"COVERAGE: required dimension '{}' lacks a withstood hypothesis",
dim.tag
));
}
dimensions.push(DimensionStatus {
dimension: dim.tag.clone(),
required: dim.required,
best_standing: best,
claims: dim_claims,
satisfied,
});
}
for c in contradictions {
if c.severity == Severity::High {
blockers.push(format!(
"CONTRADICTION: surviving high-severity contradiction '{}' ({})",
c.id, c.explanation
));
}
}
for stance in stances {
if !stance.nonconforming_claims.is_empty() {
blockers.push(format!(
"CONFORMANCE: accepted stance '{}' has {} nonconforming claim(s): {}",
stance.target,
stance.nonconforming_claims.len(),
stance.nonconforming_claims.join(", ")
));
}
}
for req in requirements {
if req.priority == Some(Priority::Must) {
if req.acceptance_criteria.is_empty() {
blockers.push(format!(
"REQUIREMENT: must-requirement '{}' has no acceptance criteria",
req.id
));
}
if req.owner.is_none() {
blockers.push(format!(
"REQUIREMENT: must-requirement '{}' has no owner",
req.id
));
}
}
}
ReadinessReport {
ready: blockers.is_empty(),
dimensions,
gaps,
blockers,
}
}
impl Default for ReadinessReport {
fn default() -> Self {
Self {
ready: false,
dimensions: Vec::new(),
gaps: Vec::new(),
blockers: vec!["EMPTY: no events".to_string()],
}
}
}