use crate::ReportContext;
use allow_core::{AllowConfig, MatchOutcome, MatchStatus};
use std::collections::{BTreeMap, BTreeSet};
pub(crate) const STATUS_COUNT_ORDER: [MatchStatus; 10] = [
MatchStatus::Matched,
MatchStatus::New,
MatchStatus::Expired,
MatchStatus::ReviewDue,
MatchStatus::Stale,
MatchStatus::Ambiguous,
MatchStatus::InvalidSelector,
MatchStatus::EvidenceMissing,
MatchStatus::MissingRequiredField,
MatchStatus::BaselineDebt,
];
pub(crate) const REVIEW_ITEM_STATUSES: [MatchStatus; 8] = [
MatchStatus::New,
MatchStatus::Expired,
MatchStatus::ReviewDue,
MatchStatus::Stale,
MatchStatus::Ambiguous,
MatchStatus::InvalidSelector,
MatchStatus::MissingRequiredField,
MatchStatus::EvidenceMissing,
];
pub(crate) const AUDIT_REVIEW_QUEUE_STATUSES: [MatchStatus; 8] = [
MatchStatus::New,
MatchStatus::Expired,
MatchStatus::Ambiguous,
MatchStatus::EvidenceMissing,
MatchStatus::MissingRequiredField,
MatchStatus::BaselineDebt,
MatchStatus::Stale,
MatchStatus::ReviewDue,
];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Summary {
pub total: usize,
pub by_status: BTreeMap<MatchStatus, usize>,
}
impl Summary {
pub fn from_outcomes(outcomes: &[MatchOutcome]) -> Self {
let mut summary = Self {
total: outcomes.len(),
by_status: BTreeMap::new(),
};
for outcome in outcomes {
*summary.by_status.entry(outcome.status).or_insert(0) += 1;
}
summary
}
pub fn count(&self, status: MatchStatus) -> usize {
*self.by_status.get(&status).unwrap_or(&0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ReviewSignals {
pub(crate) baseline_debt: usize,
pub(crate) policy_missing_evidence: usize,
pub(crate) broken_evidence_links: usize,
pub(crate) weak_evidence_references: usize,
pub(crate) review_items: usize,
}
impl ReviewSignals {
pub(crate) fn from_summary(summary: &Summary, context: ReportContext<'_>) -> Self {
let baseline_debt = baseline_debt_count(summary, context);
let policy_missing_evidence = policy_missing_evidence_count(summary, context);
let broken_evidence_links = broken_evidence_link_count(context);
let weak_evidence_references = weak_evidence_reference_count(context);
let review_items = review_item_count_with_baseline(
summary,
baseline_debt,
policy_missing_evidence,
broken_evidence_links,
weak_evidence_references,
);
Self {
baseline_debt,
policy_missing_evidence,
broken_evidence_links,
weak_evidence_references,
review_items,
}
}
}
pub(crate) fn render_count_fields_with_policy_context(
summary: &Summary,
policy_baseline_debt: Option<usize>,
policy_missing_evidence: Option<usize>,
broken_evidence_links: Option<usize>,
weak_evidence_references: Option<usize>,
indent: &str,
) -> String {
let include_policy_baseline_debt =
policy_baseline_debt.filter(|count| *count > summary.count(MatchStatus::BaselineDebt));
let include_policy_missing_evidence = policy_missing_evidence
.filter(|count| *count > summary.count(MatchStatus::EvidenceMissing));
let include_broken_evidence_links = broken_evidence_links.filter(|count| *count > 0);
let include_weak_evidence_references = weak_evidence_references.filter(|count| *count > 0);
let optional_fields = [
("policy_baseline_debt", include_policy_baseline_debt),
("policy_missing_evidence", include_policy_missing_evidence),
("broken_evidence_links", include_broken_evidence_links),
("weak_evidence_references", include_weak_evidence_references),
]
.into_iter()
.filter_map(|(name, value)| value.map(|value| (name, value)))
.collect::<Vec<_>>();
let mut out = STATUS_COUNT_ORDER
.iter()
.enumerate()
.map(|(idx, status)| {
let comma = if idx + 1 == STATUS_COUNT_ORDER.len() && optional_fields.is_empty() {
""
} else {
","
};
format!(
"{indent}\"{}\": {}{comma}\n",
status.as_str(),
summary.count(*status)
)
})
.collect::<String>();
for (index, (name, value)) in optional_fields.iter().enumerate() {
let comma = if index + 1 == optional_fields.len() {
""
} else {
","
};
out.push_str(&format!("{indent}\"{name}\": {value}{comma}\n"));
}
out
}
pub(crate) fn review_item_count_with_baseline(
summary: &Summary,
baseline_debt: usize,
policy_missing_evidence: usize,
broken_evidence_links: usize,
weak_evidence_references: usize,
) -> usize {
let policy_missing_evidence_extra =
policy_missing_evidence.saturating_sub(summary.count(MatchStatus::EvidenceMissing));
REVIEW_ITEM_STATUSES
.iter()
.map(|status| summary.count(*status))
.sum::<usize>()
+ baseline_debt
+ policy_missing_evidence_extra
+ broken_evidence_links
+ weak_evidence_references
}
pub(crate) fn baseline_debt_count(summary: &Summary, context: ReportContext<'_>) -> usize {
context
.baseline_debt_entries
.unwrap_or_else(|| summary.count(MatchStatus::BaselineDebt))
}
pub(crate) fn broken_evidence_link_count(context: ReportContext<'_>) -> usize {
context.broken_evidence_links.unwrap_or(0)
}
pub(crate) fn weak_evidence_reference_count(context: ReportContext<'_>) -> usize {
context.weak_evidence_references.unwrap_or(0)
}
pub(crate) fn policy_missing_evidence_count(
summary: &Summary,
context: ReportContext<'_>,
) -> usize {
context
.policy_missing_evidence_entries
.unwrap_or_else(|| summary.count(MatchStatus::EvidenceMissing))
}
pub fn policy_baseline_debt_entries(cfg: &AllowConfig) -> usize {
cfg.allow
.iter()
.filter(|entry| entry.classification == "baseline_debt")
.count()
}
pub fn policy_missing_evidence_entries(cfg: &AllowConfig) -> usize {
cfg.allow
.iter()
.filter(|entry| entry.evidence.is_empty())
.count()
}
pub fn matched_policy_missing_evidence_entries(
cfg: &AllowConfig,
outcomes: &[MatchOutcome],
) -> usize {
let matched_allow_ids = outcomes
.iter()
.filter(|outcome| outcome.status == MatchStatus::Matched)
.filter_map(|outcome| outcome.allow_id.as_deref())
.collect::<BTreeSet<_>>();
cfg.allow
.iter()
.filter(|entry| entry.classification != "baseline_debt")
.filter(|entry| entry.evidence.is_empty())
.filter(|entry| matched_allow_ids.contains(entry.id.as_str()))
.count()
}
pub(crate) fn audit_review_queue(outcomes: &[MatchOutcome]) -> Vec<&MatchOutcome> {
outcomes
.iter()
.filter(|outcome| outcome.status != MatchStatus::Matched)
.take(20)
.collect()
}