use serde_json::Value;
pub struct GateLine {
pub name: String,
pub status: String,
pub enforced: bool,
pub observed: Option<f64>,
pub threshold: Option<f64>,
pub threshold_label: Option<String>,
}
impl GateLine {
fn measured_clause(&self) -> String {
match (
self.observed,
self.threshold,
self.threshold_label.as_deref(),
) {
(Some(observed), Some(threshold), _) => {
format!(" ({} against {})", trim_num(observed), trim_num(threshold))
}
(Some(observed), None, Some(label)) => {
format!(" ({} at or above {label})", trim_num(observed))
}
(Some(observed), None, None) => format!(" ({})", trim_num(observed)),
_ => String::new(),
}
}
fn described(&self) -> String {
format!("{}{}", self.name, self.measured_clause())
}
fn observed_text(&self) -> String {
match (
self.observed,
self.threshold,
self.threshold_label.as_deref(),
) {
(Some(observed), Some(threshold), _) => {
format!("{} against {}", trim_num(observed), trim_num(threshold))
}
(Some(observed), None, Some(label)) => {
format!("{} at or above {label}", trim_num(observed))
}
(Some(observed), None, None) => trim_num(observed),
_ => self.status.clone(),
}
}
fn threshold_text(&self) -> Option<String> {
self.threshold
.map(trim_num)
.or_else(|| self.threshold_label.clone())
}
}
fn trim_num(value: f64) -> String {
if value.fract() == 0.0 {
format!("{value:.0}")
} else {
format!("{value}")
}
}
pub fn read_gate_outcomes(envelope: &Value) -> Vec<GateLine> {
let Some(map) = envelope.get("gate_outcomes").and_then(Value::as_object) else {
return Vec::new();
};
map.iter()
.filter_map(|(name, entry)| {
Some(GateLine {
name: name.clone(),
status: entry.get("status")?.as_str()?.to_owned(),
enforced: entry.get("enforced").and_then(Value::as_bool)?,
observed: entry.get("observed").and_then(Value::as_f64),
threshold: entry.get("threshold").and_then(Value::as_f64),
threshold_label: entry
.get("threshold_label")
.and_then(Value::as_str)
.map(str::to_owned),
})
})
.collect()
}
struct Partitioned<'a> {
failed: Vec<&'a GateLine>,
warned: Vec<&'a GateLine>,
skipped: Vec<&'a GateLine>,
passed: Vec<&'a GateLine>,
}
fn partition(gates: &[GateLine]) -> Partitioned<'_> {
let mut out = Partitioned {
failed: Vec::new(),
warned: Vec::new(),
skipped: Vec::new(),
passed: Vec::new(),
};
for gate in gates {
match gate.status.as_str() {
"fail" => out.failed.push(gate),
"skipped" => out.skipped.push(gate),
"pass" => out.passed.push(gate),
_ => out.warned.push(gate),
}
}
out
}
fn join(gates: &[&GateLine]) -> String {
gates
.iter()
.map(|gate| gate.described())
.collect::<Vec<_>>()
.join(", ")
}
pub fn summary_line(envelope: &Value) -> Option<String> {
let gates = read_gate_outcomes(envelope);
if gates.is_empty() {
return None;
}
let parts = partition(&gates);
let mut clauses: Vec<String> = Vec::new();
if !parts.failed.is_empty() {
let enforced = parts.failed.iter().filter(|gate| gate.enforced).count();
let suffix = if enforced == 0 {
" (none of which fails this run)"
} else {
""
};
clauses.push(format!("failed {}{suffix}", join(&parts.failed)));
}
if !parts.warned.is_empty() {
clauses.push(format!("warned {}", join(&parts.warned)));
}
if !parts.skipped.is_empty() {
clauses.push(format!("stood down {}", join(&parts.skipped)));
}
if !parts.passed.is_empty() {
clauses.push(format!("passed {}", join(&parts.passed)));
}
Some(format!("Gate outcomes: {}.", clauses.join("; ")))
}
pub fn summary_line_for_gates(gates: Option<&fallow_output::GateOutcomes>) -> Option<String> {
let gates = gates?;
let envelope = serde_json::json!({ "gate_outcomes": gates });
summary_line(&envelope)
}
pub fn annotation_line(envelope: &Value) -> Option<String> {
let line = summary_line(envelope)?;
Some(format!("::notice::Fallow: {line}"))
}
pub fn gate_rows(envelope: &Value) -> Vec<fallow_output::PrDecisionGate> {
read_gate_outcomes(envelope)
.iter()
.map(|gate| fallow_output::PrDecisionGate {
id: gate.name.clone(),
label: gate_label(&gate.name),
status: row_status(gate),
observed: gate.observed_text(),
threshold: gate.threshold_text(),
scope: "this run".to_owned(),
})
.collect()
}
pub fn gate_rows_for_gates(
gates: Option<&fallow_output::GateOutcomes>,
) -> Vec<fallow_output::PrDecisionGate> {
let Some(gates) = gates else {
return Vec::new();
};
gate_rows(&serde_json::json!({ "gate_outcomes": gates }))
}
fn row_status(gate: &GateLine) -> fallow_output::PrDecisionConclusion {
use fallow_output::PrDecisionConclusion as Conclusion;
match gate.status.as_str() {
"fail" if gate.enforced => Conclusion::Failure,
"pass" => Conclusion::Success,
"skipped" => Conclusion::Skipped,
_ => Conclusion::Neutral,
}
}
fn gate_label(name: &str) -> String {
known_gate_label(name).map_or_else(|| sentence_case(name), str::to_owned)
}
fn known_gate_label(name: &str) -> Option<&'static str> {
Some(match name {
"error-severity-findings" => "Error-severity findings",
"regression" => "Regression",
"stale-baseline" => "Stale baseline",
"duplication-threshold" => "Duplication threshold",
"health-min-score" => "Health minimum score",
"health-min-severity" => "Health minimum severity",
"health-findings" => "Health findings",
"health-coverage-gaps" => "Coverage gaps",
"health-runtime-coverage" => "Runtime coverage",
"security" => "Security",
"security-advisory" => "Security advisory",
"audit-verdict" => "Audit verdict",
"type-aware-require" => "Type-aware completeness",
_ => return None,
})
}
fn sentence_case(name: &str) -> String {
let spaced = name.replace('-', " ");
let mut chars = spaced.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn envelope(gates: &Value) -> Value {
serde_json::json!({ "kind": "health", "gate_outcomes": gates })
}
#[test]
fn an_envelope_without_the_object_renders_nothing() {
let bare = serde_json::json!({ "kind": "dead-code" });
assert!(summary_line(&bare).is_none());
assert!(annotation_line(&bare).is_none());
}
#[test]
fn a_failing_gate_names_what_it_compared() {
let value = envelope(&serde_json::json!({
"health-min-score": {
"status": "fail", "enforced": true, "observed": 85.0, "threshold": 90.0
}
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: failed health-min-score (85 against 90)."
);
}
#[test]
fn the_annotation_is_always_a_notice() {
for enforced in [true, false] {
let value = envelope(&serde_json::json!({
"audit-verdict": { "status": "fail", "enforced": enforced }
}));
let line = annotation_line(&value).expect("a gate ran");
assert!(
line.starts_with("::notice::"),
"the render states the fact and leaves the escalation to the consumer: {line}"
);
}
}
#[test]
fn an_unenforced_failure_says_it_does_not_fail_the_run() {
let value = envelope(&serde_json::json!({
"stale-baseline": { "status": "fail", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: failed stale-baseline (none of which fails this run)."
);
}
#[test]
fn a_stood_down_gate_is_not_a_pass() {
let value = envelope(&serde_json::json!({
"stale-baseline": { "status": "skipped", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: stood down stale-baseline."
);
}
#[test]
fn a_warn_tier_is_not_a_pass() {
let value = envelope(&serde_json::json!({
"audit-verdict": { "status": "warn", "enforced": true }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: warned audit-verdict."
);
}
#[test]
fn passing_gates_are_named_on_their_own() {
let value = envelope(&serde_json::json!({
"regression": { "status": "pass", "enforced": true }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: passed regression."
);
}
#[test]
fn all_four_outcomes_stay_apart_in_one_line() {
let value = envelope(&serde_json::json!({
"regression": { "status": "fail", "enforced": true },
"audit-verdict": { "status": "warn", "enforced": true },
"stale-baseline": { "status": "skipped", "enforced": false },
"duplication-threshold": { "status": "pass", "enforced": true }
}));
assert_eq!(
summary_line(&value).expect("gates ran"),
"Gate outcomes: failed regression; warned audit-verdict; \
stood down stale-baseline; passed duplication-threshold."
);
}
#[test]
fn a_named_floor_is_rendered_instead_of_a_number() {
let value = envelope(&serde_json::json!({
"health-min-severity": {
"status": "fail", "enforced": true, "observed": 3.0,
"threshold_label": "critical"
}
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: failed health-min-severity (3 at or above critical)."
);
}
#[test]
fn an_unrecognised_gate_name_still_reports() {
let value = envelope(&serde_json::json!({
"some-future-gate": { "status": "fail", "enforced": true }
}));
assert_eq!(
annotation_line(&value).expect("a gate ran"),
"::notice::Fallow: Gate outcomes: failed some-future-gate."
);
}
#[test]
fn an_unrecognised_status_is_not_a_pass() {
let value = envelope(&serde_json::json!({
"some-future-gate": { "status": "deferred", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: warned some-future-gate."
);
}
#[test]
fn a_mix_of_enforced_and_unenforced_failures_adds_no_note() {
let value = envelope(&serde_json::json!({
"regression": { "status": "fail", "enforced": true },
"stale-baseline": { "status": "fail", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("gates ran"),
"Gate outcomes: failed regression, stale-baseline."
);
}
#[test]
fn an_envelope_without_the_object_builds_no_rows() {
assert!(gate_rows(&serde_json::json!({ "kind": "dead-code" })).is_empty());
assert!(gate_rows_for_gates(None).is_empty());
}
#[test]
fn an_armed_stale_baseline_gate_becomes_a_failing_row() {
let value = envelope(&serde_json::json!({
"stale-baseline": { "status": "fail", "enforced": true }
}));
let rows = gate_rows(&value);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].id, "stale-baseline");
assert_eq!(rows[0].label, "Stale baseline");
assert_eq!(rows[0].status, fallow_output::PrDecisionConclusion::Failure);
assert_eq!(rows[0].observed, "fail");
assert_eq!(rows[0].threshold, None);
assert_eq!(rows[0].scope, "this run");
}
#[test]
fn the_four_statuses_map_onto_the_check_run_conclusions() {
use fallow_output::PrDecisionConclusion as Conclusion;
for (status, enforced, expected) in [
("fail", true, Conclusion::Failure),
("fail", false, Conclusion::Neutral),
("warn", true, Conclusion::Neutral),
("skipped", false, Conclusion::Skipped),
("pass", true, Conclusion::Success),
("deferred", true, Conclusion::Neutral),
] {
let value = envelope(&serde_json::json!({
"stale-baseline": { "status": status, "enforced": enforced }
}));
let rows = gate_rows(&value);
assert_eq!(rows[0].status, expected, "{status} enforced={enforced}");
assert_eq!(
rows[0].observed, status,
"{status} must say what it concluded"
);
}
}
#[test]
fn a_measured_gate_carries_both_numbers() {
let value = envelope(&serde_json::json!({
"health-min-score": {
"status": "fail", "enforced": true, "observed": 85.0, "threshold": 90.0
}
}));
let rows = gate_rows(&value);
assert_eq!(rows[0].observed, "85 against 90");
assert_eq!(rows[0].threshold.as_deref(), Some("90"));
}
#[test]
fn a_named_floor_is_carried_as_the_threshold() {
let value = envelope(&serde_json::json!({
"health-min-severity": {
"status": "fail", "enforced": true, "observed": 3.0,
"threshold_label": "critical"
}
}));
let rows = gate_rows(&value);
assert_eq!(rows[0].observed, "3 at or above critical");
assert_eq!(rows[0].threshold.as_deref(), Some("critical"));
}
#[test]
fn an_unrecognised_gate_name_still_builds_a_readable_row() {
let value = envelope(&serde_json::json!({
"some-future-gate": { "status": "fail", "enforced": true }
}));
let rows = gate_rows(&value);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].id, "some-future-gate");
assert_eq!(rows[0].label, "Some future gate");
}
#[test]
fn every_gate_this_build_emits_has_its_own_label() {
for name in fallow_output::GateName::ALL {
let key = name.as_str();
assert!(
known_gate_label(key).is_some(),
"{key} falls through to the open-set fallback"
);
}
}
#[test]
fn the_live_and_saved_rows_agree() {
let mut gates = fallow_output::GateOutcomes::new();
gates.insert(
fallow_output::GateName::StaleBaseline,
fallow_output::GateOutcome::new(fallow_output::GateStatus::Fail, true),
);
gates.insert(
fallow_output::GateName::HealthMinScore,
fallow_output::GateOutcome::measured(fallow_output::GateStatus::Fail, true, 85.0, 90.0),
);
let envelope = serde_json::json!({ "gate_outcomes": gates });
assert_eq!(gate_rows(&envelope), gate_rows_for_gates(Some(&gates)));
}
}