Skip to main content

libverify_core/controls/
description_quality.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, GovernedChange};
3
4/// Minimum body length (in characters) for a change request description.
5const MIN_BODY_LENGTH: usize = 10;
6
7/// Verifies that change requests include a meaningful description.
8///
9/// Maps to SOC2 CC8.1: change management documentation.
10/// A well-documented change request ensures reviewers understand intent,
11/// scope, and rationale before approving.
12pub struct DescriptionQualityControl;
13
14impl Control for DescriptionQualityControl {
15    fn id(&self) -> ControlId {
16        builtin::id(builtin::DESCRIPTION_QUALITY)
17    }
18
19    fn description(&self) -> &'static str {
20        "Change requests must include a meaningful description"
21    }
22
23    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
24        if evidence.change_requests.is_empty() {
25            return vec![ControlFinding::not_applicable(
26                self.id(),
27                "No change requests found",
28            )];
29        }
30
31        evidence
32            .change_requests
33            .iter()
34            .map(|cr| evaluate_change(self.id(), cr))
35            .collect()
36    }
37}
38
39fn evaluate_change(id: ControlId, cr: &GovernedChange) -> ControlFinding {
40    let cr_subject = cr.id.to_string();
41
42    let body = cr.summary.as_deref().unwrap_or("").trim();
43
44    if body.is_empty() {
45        return ControlFinding::violated(
46            id,
47            format!("{cr_subject}: change request has no description"),
48            vec![cr_subject],
49        );
50    }
51
52    if body.len() < MIN_BODY_LENGTH {
53        return ControlFinding::violated(
54            id,
55            format!(
56                "{cr_subject}: description too short ({} chars, minimum {MIN_BODY_LENGTH})",
57                body.len()
58            ),
59            vec![cr_subject],
60        );
61    }
62
63    ControlFinding::satisfied(
64        id,
65        format!("{cr_subject}: description present ({} chars)", body.len()),
66        vec![cr_subject],
67    )
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::control::ControlStatus;
74    use crate::evidence::{ChangeRequestId, EvidenceState};
75
76    fn make_change(summary: Option<&str>) -> GovernedChange {
77        GovernedChange {
78            id: ChangeRequestId::new("test", "owner/repo#1"),
79            title: "test".to_string(),
80            summary: summary.map(|s| s.to_string()),
81            submitted_by: None,
82            changed_assets: EvidenceState::not_applicable(),
83            approval_decisions: EvidenceState::not_applicable(),
84            source_revisions: EvidenceState::not_applicable(),
85            work_item_refs: EvidenceState::not_applicable(),
86        }
87    }
88
89    fn bundle(changes: Vec<GovernedChange>) -> EvidenceBundle {
90        EvidenceBundle {
91            change_requests: changes,
92            ..Default::default()
93        }
94    }
95
96    #[test]
97    fn not_applicable_when_no_changes() {
98        let findings = DescriptionQualityControl.evaluate(&EvidenceBundle::default());
99        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
100    }
101
102    #[test]
103    fn satisfied_when_body_present() {
104        let cr = make_change(Some(
105            "This PR adds a new compliance control for description quality.",
106        ));
107        let findings = DescriptionQualityControl.evaluate(&bundle(vec![cr]));
108        assert_eq!(findings[0].status, ControlStatus::Satisfied);
109    }
110
111    #[test]
112    fn violated_when_body_none() {
113        let cr = make_change(None);
114        let findings = DescriptionQualityControl.evaluate(&bundle(vec![cr]));
115        assert_eq!(findings[0].status, ControlStatus::Violated);
116        assert!(findings[0].rationale.contains("no description"));
117    }
118
119    #[test]
120    fn violated_when_body_empty() {
121        let cr = make_change(Some(""));
122        let findings = DescriptionQualityControl.evaluate(&bundle(vec![cr]));
123        assert_eq!(findings[0].status, ControlStatus::Violated);
124    }
125
126    #[test]
127    fn violated_when_body_too_short() {
128        let cr = make_change(Some("fix"));
129        let findings = DescriptionQualityControl.evaluate(&bundle(vec![cr]));
130        assert_eq!(findings[0].status, ControlStatus::Violated);
131        assert!(findings[0].rationale.contains("too short"));
132    }
133
134    #[test]
135    fn violated_when_body_only_whitespace() {
136        let cr = make_change(Some("   \n\t  "));
137        let findings = DescriptionQualityControl.evaluate(&bundle(vec![cr]));
138        assert_eq!(findings[0].status, ControlStatus::Violated);
139        assert!(findings[0].rationale.contains("no description"));
140    }
141}