use std::path::Path;
use crate::compare::{compare_states, load_state, RepositoryComparison};
use crate::intent::{
analyze_intent, IntentFinding, ACCEPTANCE_CRITERIA_REMOVED, CONSTRAINT_REMOVED,
CONSTRAINT_WEAKENED, SEVERITY_WARNING, SPECIFICITY_REGRESSION, SUCCESS_MEASURES_REMOVED,
};
use crate::pycompat::{py_abspath, py_relpath};
use crate::revisions::{materialize_revision, repository_root, MaterializedRevision, RevisionError};
pub const REASON_VALIDATION_REGRESSION: &str = "validation_regression";
pub const REASON_BROKEN_RELATIONSHIP: &str = "broken_relationship";
pub const RECOMMENDING_FINDINGS: [&str; 5] = [
SPECIFICITY_REGRESSION,
CONSTRAINT_WEAKENED,
CONSTRAINT_REMOVED,
ACCEPTANCE_CRITERIA_REMOVED,
SUCCESS_MEASURES_REMOVED,
];
pub fn is_recommending(code: &str) -> bool {
RECOMMENDING_FINDINGS.contains(&code)
}
fn reason_text(code: &str) -> &'static str {
match code {
REASON_VALIDATION_REGRESSION => "One or more artifacts became invalid.",
REASON_BROKEN_RELATIONSHIP => "One or more relationship references broke.",
SPECIFICITY_REGRESSION => "A measurable requirement became vague.",
CONSTRAINT_WEAKENED => "A mandatory requirement was weakened.",
CONSTRAINT_REMOVED => "A requirement with mandatory wording was removed.",
ACCEPTANCE_CRITERIA_REMOVED => "An acceptance criteria section was removed.",
SUCCESS_MEASURES_REMOVED => "A success measures section was removed.",
_ => "",
}
}
#[derive(Debug, Clone)]
pub struct ReviewRecommendation {
pub code: &'static str,
pub reason: &'static str,
}
pub struct WatchkeeperReport {
pub directory: String,
pub base: String, pub head: String, pub comparison: RepositoryComparison,
pub findings: Vec<IntentFinding>,
pub recommendations: Vec<ReviewRecommendation>,
}
impl WatchkeeperReport {
pub fn review_recommended(&self) -> bool {
!self.recommendations.is_empty()
}
pub fn has_warnings(&self) -> bool {
self.findings
.iter()
.any(|f| f.severity == SEVERITY_WARNING)
}
}
pub fn derive_recommendations(
comparison: &RepositoryComparison,
findings: &[IntentFinding],
) -> Vec<ReviewRecommendation> {
let mut codes: Vec<&'static str> = Vec::new();
if !comparison.validation.newly_invalid.is_empty() {
codes.push(REASON_VALIDATION_REGRESSION);
}
if !comparison.relationships.new_issues.is_empty() {
codes.push(REASON_BROKEN_RELATIONSHIP);
}
for finding in findings {
if is_recommending(finding.code) && !codes.contains(&finding.code) {
codes.push(finding.code);
}
}
codes
.into_iter()
.map(|code| ReviewRecommendation {
code,
reason: reason_text(code),
})
.collect()
}
fn resolve_side(
guards: &mut Vec<MaterializedRevision>,
directory: &str,
reference: &str,
) -> Result<String, RevisionError> {
if Path::new(reference).is_dir() {
return Ok(reference.to_string());
}
let root = repository_root(directory)?;
let subpath = py_relpath(&py_abspath(directory), &root);
let materialized = materialize_revision(&root, reference, &subpath)?;
let corpus = materialized.corpus.to_string_lossy().into_owned();
guards.push(materialized);
Ok(corpus)
}
pub fn build_watchkeeper_report(
directory: &str,
base: &str,
head: Option<&str>,
) -> Result<WatchkeeperReport, RevisionError> {
let head_label = head.unwrap_or(directory);
let mut guards: Vec<MaterializedRevision> = Vec::new();
let base_dir = resolve_side(&mut guards, directory, base)?;
let head_dir = match head {
Some(reference) => resolve_side(&mut guards, directory, reference)?,
None => directory.to_string(),
};
let base_state = load_state(&base_dir, base);
let head_state = load_state(&head_dir, head_label);
let comparison = compare_states(base_state, head_state);
let findings = analyze_intent(&comparison);
drop(guards); let recommendations = derive_recommendations(&comparison, &findings);
Ok(WatchkeeperReport {
directory: directory.to_string(),
base: base.to_string(),
head: head_label.to_string(),
comparison,
findings,
recommendations,
})
}