use super::diagnostics::{HealthDiagnostic, HealthDiagnosticKind};
use super::events::{HealthScope, HealthSource};
use super::index::HealthIndex;
use super::policy::HealthPolicy;
use super::state::HealthState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Contribution {
pub number: u32,
pub state: HealthState,
pub recorded: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HealthRollup {
pub scope: HealthScope,
pub state: HealthState,
pub source: HealthSource,
pub policy_version: String,
pub contributions: Vec<Contribution>,
pub diagnostics: Vec<HealthDiagnostic>,
}
impl HealthRollup {
pub fn is_derived(&self) -> bool {
!self.contributions.is_empty()
}
pub fn contributing(&self) -> usize {
self.contributions.len()
}
pub fn render(&self) -> String {
if self.contributions.is_empty() {
return format!(
"{} ({}: nothing contributes to `{}` yet)",
self.state.label(),
self.policy_version,
self.scope
);
}
let tally: Vec<String> = self
.contributions
.iter()
.map(|contribution| format!("#{} {}", contribution.number, contribution.state.as_str()))
.collect();
format!(
"{} ({}, {}, {} contributor{}: {})",
self.state.label(),
self.policy_version,
self.source.as_str(),
self.contributions.len(),
if self.contributions.len() == 1 {
""
} else {
"s"
},
tally.join(", ")
)
}
pub fn provenance(&self) -> String {
format!("{} ({})", self.policy_version, self.source.as_str())
}
}
pub fn rollup(index: &HealthIndex, scope: &HealthScope, policy: &HealthPolicy) -> HealthRollup {
let contributions: Vec<Contribution> = index
.members_of(scope)
.iter()
.map(|number| {
let (state, recorded) = index
.state_of(*number)
.unwrap_or((HealthState::Unknown, None));
let state = match (state, recorded.is_some()) {
(HealthState::Unknown, true) => policy.missing_reference(),
(state, _) => state,
};
Contribution {
number: *number,
state,
recorded,
}
})
.collect();
let state = match policy.worst(contributions.iter().map(|contribution| contribution.state)) {
Some(state) => state,
None => policy.empty_scope(),
};
let mut diagnostics: Vec<HealthDiagnostic> =
index.diagnostics_for(scope).into_iter().cloned().collect();
for contribution in &contributions {
if contribution.state != HealthState::Unknown {
continue;
}
let message = match &contribution.recorded {
Some(raw) => format!(
"issue #{} records health `{raw}`, which this build cannot read",
contribution.number
),
None => format!("issue #{} records no health", contribution.number),
};
let mut finding =
HealthDiagnostic::new(HealthDiagnosticKind::UnknownHealth, scope.clone(), message);
if let Some(raw) = &contribution.recorded {
finding = finding.carrying(raw.clone());
}
diagnostics.push(finding);
}
HealthRollup {
scope: scope.clone(),
state,
source: HealthSource::Derived,
policy_version: policy.version().to_string(),
contributions,
diagnostics,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Issue;
fn issue(number: u32, parent: Option<u32>, health: Option<&str>) -> Issue {
let parent = parent.map_or(String::new(), |p| format!("parent: {p}\n"));
let health = health
.map(|state| format!("health:\n state: {state}\n"))
.unwrap_or_default();
let contents = format!(
"---\nnumber: {number}\ntitle: \"Issue {number}\"\nstatus: \"todo\"\n\
created: \"2026-01-01\"\nupdated: \"2026-01-01\"\n{parent}{health}---\n\nBody.\n"
);
let mut issue = Issue::parse(&contents).expect("the fixture parses");
issue.filename = format!("issue-{number}.md");
issue
}
#[test]
fn health_rollup_takes_the_worst_known_contribution() {
let policy = HealthPolicy::v1();
let issues = vec![
issue(1, None, None),
issue(2, Some(1), Some("on_track")),
issue(3, Some(1), Some("needs_attention")),
issue(4, Some(1), Some("unknown")),
];
let index = HealthIndex::build(&issues, &[], &[], &policy);
let value = rollup(&index, &HealthScope::Issue(1), &policy);
assert_eq!(value.state, HealthState::NeedsAttention);
assert_eq!(value.contributing(), 3);
assert!(value.is_derived());
assert_eq!(value.source, HealthSource::Derived);
assert_eq!(value.policy_version, "health-v1");
let rendered = value.render();
assert!(rendered.contains("Needs attention"), "{rendered}");
assert!(rendered.contains("#3 needs_attention"), "{rendered}");
assert!(rendered.contains("3 contributors"), "{rendered}");
let issues = vec![
issue(1, None, None),
issue(2, Some(1), Some("on_track")),
issue(3, Some(1), Some("at_risk")),
];
let index = HealthIndex::build(&issues, &[], &[], &policy);
assert_eq!(
rollup(&index, &HealthScope::Issue(1), &policy).state,
HealthState::AtRisk
);
}
#[test]
fn health_rollup_of_nothing_and_of_unknowns_is_unknown() {
let policy = HealthPolicy::v1();
let issues = vec![issue(1, None, Some("on_track"))];
let index = HealthIndex::build(&issues, &[], &[], &policy);
let empty = rollup(&index, &HealthScope::Issue(9), &policy);
assert_eq!(empty.state, HealthState::Unknown);
assert!(!empty.is_derived());
assert!(
empty.render().contains("nothing contributes"),
"{}",
empty.render()
);
let issues = vec![
issue(1, None, None),
issue(2, Some(1), None),
issue(3, Some(1), Some("unknown")),
];
let index = HealthIndex::build(&issues, &[], &[], &policy);
let unknowns = rollup(&index, &HealthScope::Issue(1), &policy);
assert_eq!(unknowns.state, HealthState::Unknown);
assert_eq!(unknowns.contributing(), 2);
assert!(
unknowns.is_derived(),
"there are contributors, they just say nothing"
);
}
#[test]
fn health_rollup_reports_an_unreadable_contribution_as_the_policy_says() {
let policy = HealthPolicy::v1();
let issues = vec![
issue(1, None, None),
issue(2, Some(1), Some("on_track")),
issue(3, Some(1), Some("fine")),
];
let index = HealthIndex::build(&issues, &[], &[], &policy);
let value = rollup(&index, &HealthScope::Issue(1), &policy);
assert_eq!(value.state, HealthState::OnTrack);
let unreadable = value
.contributions
.iter()
.find(|c| c.recorded.as_deref() == Some("fine"))
.expect("the unreadable contribution is present");
assert_eq!(unreadable.state, HealthState::Unknown);
assert!(value.render().contains("#3 unknown"), "{}", value.render());
assert_eq!(value.diagnostics.len(), 1);
assert_eq!(value.diagnostics[0].kind.as_str(), "unknown_health");
assert!(
value.diagnostics[0].message.contains("`fine`"),
"{}",
value.diagnostics[0].message
);
assert_eq!(value.diagnostics[0].state.as_deref(), Some("fine"));
assert!(value.provenance().contains("health-v1"));
}
#[test]
fn health_rollup_names_a_missing_reference_without_hiding_it() {
let policy = HealthPolicy::v1();
let issues = vec![
issue(1, None, None),
issue(2, Some(1), Some("on_track")),
issue(3, Some(99), Some("at_risk")),
];
let index = HealthIndex::build(&issues, &[], &[], &policy);
let missing = rollup(&index, &HealthScope::Issue(99), &policy);
assert_eq!(
missing.contributing(),
1,
"the unresolvable child still counts"
);
assert_eq!(
missing.state,
HealthState::AtRisk,
"and its own state is reported"
);
assert_eq!(missing.diagnostics[0].kind.as_str(), "missing_child");
assert!(
missing.diagnostics[0].message.contains("#3"),
"{}",
missing.diagnostics[0].message
);
}
}