use regex::Regex;
use crate::plan::{Plan, VerificationCriterion};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum VtVerdict {
Pass,
Fail { reason: String },
Uncheckable,
Waived { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct VtLine {
pub id: String,
pub verdict: VtVerdict,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PhaseVtReport {
pub phase_id: String,
pub lines: Vec<VtLine>,
}
const NO_REASON: &str = "(no reason recorded)";
pub(crate) fn check_vt(
vt: &VerificationCriterion,
read_file: &impl Fn(&str) -> Option<String>,
) -> VtVerdict {
if vt.waived {
let reason = vt
.waived_reason
.clone()
.unwrap_or_else(|| NO_REASON.to_string());
return VtVerdict::Waived { reason };
}
let Some(path) = vt.test_file.as_deref() else {
return VtVerdict::Uncheckable;
};
let Some(source) = read_file(path) else {
return VtVerdict::Fail {
reason: format!("mandated test_file `{path}` not found"),
};
};
for kw in &vt.keywords {
if !source.contains(kw.as_str()) {
return VtVerdict::Fail {
reason: format!("keyword `{kw}` absent from `{path}`"),
};
}
}
for pat in &vt.patterns {
match Regex::new(pat) {
Ok(re) => {
if !source.lines().any(|line| re.is_match(line)) {
return VtVerdict::Fail {
reason: format!("pattern `{pat}` matched no line in `{path}`"),
};
}
}
Err(_) => {
return VtVerdict::Fail {
reason: format!("pattern `{pat}` is not a valid regex"),
};
}
}
}
VtVerdict::Pass
}
pub(crate) fn check_phases(
plan: &Plan,
read_file: &impl Fn(&str) -> Option<String>,
) -> Vec<PhaseVtReport> {
plan.phases
.iter()
.map(|ph| PhaseVtReport {
phase_id: ph.id.clone(),
lines: ph
.verification
.iter()
.filter(|vt| is_vt_mode(&vt.id))
.map(|vt| VtLine {
id: vt.id.clone(),
verdict: check_vt(vt, read_file),
})
.collect(),
})
.collect()
}
pub(crate) fn has_failure(reports: &[PhaseVtReport]) -> bool {
reports
.iter()
.flat_map(|r| r.lines.iter())
.any(|l| matches!(l.verdict, VtVerdict::Fail { .. }))
}
fn is_vt_mode(id: &str) -> bool {
id.starts_with("VT-") || id == "VT"
}
const GLYPH_PASS: &str = "✓";
const GLYPH_FAIL: &str = "✗";
const GLYPH_UNCHECKABLE: &str = "?";
const GLYPH_WAIVED: &str = "~";
const LABEL_PASS: &str = "PASS";
const LABEL_FAIL: &str = "FAIL";
const LABEL_UNCHECKABLE: &str = "UNCHECKABLE";
const LABEL_WAIVED: &str = "WAIVED";
pub(crate) fn render_summary(reports: &[PhaseVtReport]) -> String {
let mut lines: Vec<String> = vec!["VT verification summary:".to_string()];
if reports.iter().all(|r| r.lines.is_empty()) {
lines.push(" (no VT criteria to check)".to_string());
}
for report in reports {
if report.lines.is_empty() {
continue;
}
lines.push(format!(" {}:", report.phase_id));
lines.extend(
report
.lines
.iter()
.map(|l| format!(" {}", render_line(l))),
);
}
let mut out = lines.join("\n");
out.push('\n');
out
}
fn render_line(line: &VtLine) -> String {
let id = &line.id;
match &line.verdict {
VtVerdict::Pass => format!("{GLYPH_PASS} {LABEL_PASS} {id}"),
VtVerdict::Fail { reason } => format!("{GLYPH_FAIL} {LABEL_FAIL} {id} — {reason}"),
VtVerdict::Uncheckable => {
format!("{GLYPH_UNCHECKABLE} {LABEL_UNCHECKABLE} {id} — no structured mandate")
}
VtVerdict::Waived { reason } => {
format!("{GLYPH_WAIVED} {LABEL_WAIVED} {id} — {reason}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plan::{Plan, PlanPhase, VerificationCriterion};
use std::cell::Cell;
fn vt(id: &str) -> VerificationCriterion {
VerificationCriterion {
id: id.to_string(),
expects: String::new(),
test_file: None,
keywords: vec![],
patterns: vec![],
waived: false,
waived_reason: None,
}
}
fn one(path: &'static str, source: &'static str) -> impl Fn(&str) -> Option<String> {
move |p: &str| (p == path).then(|| source.to_string())
}
#[test]
fn pass_when_file_exists_and_keyword_present() {
let mut c = vt("VT-1");
c.test_file = Some("a.rs".to_string());
c.keywords = vec!["check_vt".to_string()];
let verdict = check_vt(&c, &one("a.rs", "fn check_vt() {}"));
assert_eq!(verdict, VtVerdict::Pass);
}
#[test]
fn fail_when_mandated_file_absent() {
let mut c = vt("VT-1");
c.test_file = Some("missing.rs".to_string());
c.keywords = vec!["whatever".to_string()];
let verdict = check_vt(&c, &no_files);
assert!(matches!(verdict, VtVerdict::Fail { .. }));
}
#[test]
fn fail_when_keyword_absent_from_source() {
let mut c = vt("VT-1");
c.test_file = Some("a.rs".to_string());
c.keywords = vec!["nonexistent".to_string()];
let verdict = check_vt(&c, &one("a.rs", "fn check_vt() {}"));
assert!(matches!(verdict, VtVerdict::Fail { .. }));
}
#[test]
fn uncheckable_when_keywords_but_no_test_file() {
let mut c = vt("VT-1");
c.keywords = vec!["x".to_string()]; assert_eq!(check_vt(&c, &no_files), VtVerdict::Uncheckable);
}
#[test]
fn waived_short_circuits_before_any_read() {
let mut c = vt("VT-1");
c.waived = true;
c.waived_reason = Some("infeasible — see /consult".to_string());
c.test_file = Some("a.rs".to_string());
let touched = Cell::new(false);
let reader = |_: &str| -> Option<String> {
touched.set(true);
None
};
let verdict = check_vt(&c, &reader);
assert!(!touched.get(), "waiver must short-circuit before fs read");
assert_eq!(
verdict,
VtVerdict::Waived {
reason: "infeasible — see /consult".to_string()
}
);
}
#[test]
fn existence_only_mandate_passes_without_keywords() {
let mut c = vt("VT-1");
c.test_file = Some("a.rs".to_string()); assert_eq!(check_vt(&c, &one("a.rs", "anything")), VtVerdict::Pass);
}
#[test]
fn pattern_line_anchored_match() {
let mut c = vt("VT-1");
c.test_file = Some("a.rs".to_string());
c.patterns = vec![r"^\s*fn check_vt".to_string()];
assert_eq!(
check_vt(&c, &one("a.rs", " fn check_vt() {}")),
VtVerdict::Pass
);
assert!(matches!(
check_vt(&c, &one("a.rs", "let x = fn check_vt;")),
VtVerdict::Fail { .. }
));
}
#[test]
fn keyword_as_string_argument_satisfies() {
let mut c = vt("VT-1");
c.test_file = Some("e2e.rs".to_string());
c.keywords = vec!["check".to_string(), "regression".to_string()];
let src = r#"cmd.arg("check").arg("regression");"#;
assert_eq!(check_vt(&c, &one("e2e.rs", src)), VtVerdict::Pass);
}
#[test]
fn pattern_escalation_fails_when_shape_absent() {
let mut c = vt("VT-1");
c.test_file = Some("a.rs".to_string());
c.patterns = vec![r"^\s*assert_eq!\(.*census".to_string()];
assert_eq!(
check_vt(&c, &one("a.rs", " assert_eq!(x, census);")),
VtVerdict::Pass
);
assert!(matches!(
check_vt(&c, &one("a.rs", "let census = 1;")),
VtVerdict::Fail { .. }
));
}
fn phase(id: &str, vts: Vec<VerificationCriterion>) -> PlanPhase {
PlanPhase {
id: id.to_string(),
name: String::new(),
objective: String::new(),
entrance_criteria: vec![],
exit_criteria: vec![],
verification: vts,
}
}
#[test]
fn va_vh_only_phase_emits_no_vt_lines() {
let plan = Plan {
phases: vec![phase("PHASE-01", vec![vt("VA-1"), vt("VH-1")])],
};
let reports = check_phases(&plan, &no_files);
assert_eq!(reports.len(), 1);
assert!(reports[0].lines.is_empty(), "VA/VH are never gated");
assert!(!has_failure(&reports));
}
#[test]
fn empty_plan_yields_empty_report_no_failure() {
let plan = Plan { phases: vec![] };
let reports = check_phases(&plan, &no_files);
assert!(reports.is_empty());
assert!(!has_failure(&reports));
}
#[test]
fn has_failure_true_when_a_vt_fails() {
let mut c = vt("VT-1");
c.test_file = Some("missing.rs".to_string());
let plan = Plan {
phases: vec![phase("PHASE-01", vec![c])],
};
let reports = check_phases(&plan, &no_files);
assert!(has_failure(&reports));
}
#[test]
fn render_surfaces_all_four_verdicts() {
let reports = vec![PhaseVtReport {
phase_id: "PHASE-01".to_string(),
lines: vec![
VtLine {
id: "VT-1".to_string(),
verdict: VtVerdict::Pass,
},
VtLine {
id: "VT-2".to_string(),
verdict: VtVerdict::Fail {
reason: "test_file missing.rs not found".to_string(),
},
},
VtLine {
id: "VT-3".to_string(),
verdict: VtVerdict::Uncheckable,
},
VtLine {
id: "VT-4".to_string(),
verdict: VtVerdict::Waived {
reason: "infeasible".to_string(),
},
},
],
}];
let out = render_summary(&reports);
assert!(out.contains(LABEL_PASS));
assert!(out.contains(LABEL_FAIL));
assert!(out.contains(LABEL_UNCHECKABLE));
assert!(out.contains(LABEL_WAIVED));
assert!(out.contains("missing.rs"), "the Fail reason must surface");
assert!(out.contains("infeasible"), "the Waived reason must surface");
}
#[test]
fn render_notes_empty_when_no_vt() {
let out = render_summary(&[]);
assert!(out.contains("no VT criteria"));
}
fn no_files(_: &str) -> Option<String> {
None
}
}