libverify_core/controls/
description_quality.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, GovernedChange};
3
4const MIN_BODY_LENGTH: usize = 10;
6
7pub 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}