use std::path::Path;
use fallow_config::{WorkspaceDiagnostic, WorkspaceDiagnosticKind};
use fallow_output::{GateFile, GateName, GateOutcome, GateOutcomes, GateStatus};
pub const fn status_of(failed: bool) -> GateStatus {
if failed {
GateStatus::Fail
} else {
GateStatus::Pass
}
}
pub fn regression_outcome(
outcome: Option<&crate::regression::RegressionOutcome>,
enforced: bool,
) -> Option<GateOutcome> {
#[expect(
clippy::cast_precision_loss,
reason = "issue counts never approach the f64 integer limit"
)]
fn delta(baseline: usize, current: usize) -> f64 {
current as f64 - baseline as f64
}
let outcome = outcome?;
Some(match outcome {
crate::regression::RegressionOutcome::Pass {
baseline_total,
current_total,
} => GateOutcome {
status: GateStatus::Pass,
enforced,
observed: Some(delta(*baseline_total, *current_total)),
threshold: None,
threshold_label: None,
files: Vec::new(),
},
crate::regression::RegressionOutcome::Exceeded {
baseline_total,
current_total,
tolerance,
..
} => GateOutcome {
status: GateStatus::Fail,
enforced,
observed: Some(delta(*baseline_total, *current_total)),
threshold: Some(tolerance.allowed_delta(*baseline_total)),
threshold_label: Some(tolerance.label()),
files: Vec::new(),
},
crate::regression::RegressionOutcome::Skipped { .. } => {
GateOutcome::new(GateStatus::Skipped, enforced)
}
})
}
pub const fn stale_baseline_outcome(
staleness: Option<&fallow_output::BaselineStaleness>,
fail_on_stale_baseline: bool,
) -> Option<GateOutcome> {
let Some(staleness) = staleness else {
return None;
};
if staleness.change_scoped && !staleness.unrecognised_format {
return Some(GateOutcome::new(GateStatus::Skipped, false));
}
Some(GateOutcome::new(
status_of(staleness.gate_trips),
fail_on_stale_baseline,
))
}
pub fn type_aware_outcome(
require: fallow_config::TypeAwareRequire,
meta: Option<&fallow_types::envelope::TypeAwareMeta>,
) -> Option<GateOutcome> {
if require != fallow_config::TypeAwareRequire::Complete {
return None;
}
let incomplete = crate::report::ci::required_type_aware_incomplete(meta);
Some(GateOutcome::new(status_of(incomplete), true))
}
pub fn type_aware_meta_outcome(
meta: Option<&fallow_types::envelope::TypeAwareMeta>,
) -> Option<GateOutcome> {
let required = meta?.required_completeness
== Some(fallow_types::semantic::SemanticCompletenessRequirement::Complete);
required.then(|| {
GateOutcome::new(
status_of(crate::report::ci::required_type_aware_incomplete(meta)),
true,
)
})
}
pub fn parse_degraded_files(root: &Path, diagnostics: &[WorkspaceDiagnostic]) -> Vec<GateFile> {
let mut files: Vec<GateFile> = diagnostics
.iter()
.filter_map(|diagnostic| match diagnostic.kind {
WorkspaceDiagnosticKind::SourceParseDegraded {
error_count,
panicked,
} => Some(GateFile {
path: fallow_types::path_util::display_relative(root, &diagnostic.path),
error_count,
panicked,
}),
_ => None,
})
.collect();
files.sort_by(|a, b| a.path.cmp(&b.path));
files.dedup_by(|a, b| a.path == b.path);
files
}
pub fn parse_error_outcome(armed: bool, enforced: bool, files: &[GateFile]) -> Option<GateOutcome> {
armed.then(|| GateOutcome::with_files(status_of(!files.is_empty()), enforced, files.to_vec()))
}
pub fn sections_parse_error_outcome(
sections: &[(&fallow_config::ResolvedConfig, &[WorkspaceDiagnostic])],
) -> Option<GateOutcome> {
if !sections
.iter()
.any(|(config, _)| config.fail_on_parse_error)
{
return None;
}
let (first, _) = sections.first()?;
let diagnostics: Vec<WorkspaceDiagnostic> = sections
.iter()
.flat_map(|(_, diagnostics)| diagnostics.iter().cloned())
.collect();
parse_error_outcome(true, true, &parse_degraded_files(&first.root, &diagnostics))
}
pub fn parse_error_reason(file: &GateFile) -> String {
let errors = if file.error_count == 1 {
"1 parser error".to_owned()
} else {
format!("{} parser errors", file.error_count)
};
let outcome = if file.panicked {
"the parser stopped"
} else {
"the parser recovered"
};
format!("{errors}, {outcome}")
}
pub fn print_parse_error_gate_failure(files: &[GateFile]) {
if files.is_empty() {
return;
}
let noun = if files.len() == 1 { "file" } else { "files" };
eprintln!(
"{}",
crate::report::human_status_line(
crate::report::HumanStatus::Failure,
format_args!(
"Parse-error gate failed: fallow could not parse {} {noun} cleanly.",
files.len()
)
)
);
for file in files {
eprintln!(" {}: {}", file.path, parse_error_reason(file));
}
}
pub const fn error_severity_outcome(has_error_severity: bool, enforced: bool) -> GateOutcome {
GateOutcome::new(status_of(has_error_severity), enforced)
}
pub const fn health_findings_outcome(has_findings: bool, enforced: bool) -> GateOutcome {
GateOutcome::new(status_of(has_findings), enforced)
}
pub fn duplication_threshold_outcome(
threshold: f64,
duplication_percentage: f64,
enforced: bool,
) -> Option<GateOutcome> {
if threshold <= 0.0 {
return None;
}
Some(GateOutcome::measured(
status_of(crate::dupes::exceeds_threshold(
threshold,
duplication_percentage,
)),
enforced,
duplication_percentage,
threshold,
))
}
pub struct CheckGateInputs<'a> {
pub has_error_severity: bool,
pub regression: Option<&'a crate::regression::RegressionOutcome>,
pub baseline_staleness: Option<&'a fallow_output::BaselineStaleness>,
pub fail_on_stale_baseline: bool,
pub type_aware_require: fallow_config::TypeAwareRequire,
pub type_aware_meta: Option<&'a fallow_types::envelope::TypeAwareMeta>,
pub parse_error: Option<GateOutcome>,
}
pub fn check_gate_outcomes(input: &CheckGateInputs<'_>) -> Option<GateOutcomes> {
let mut gates = GateOutcomes::new();
gates.insert_if(
GateName::Regression,
regression_outcome(input.regression, true),
);
gates.insert_if(
GateName::StaleBaseline,
stale_baseline_outcome(input.baseline_staleness, input.fail_on_stale_baseline),
);
gates.insert_if(
GateName::TypeAwareRequire,
type_aware_outcome(input.type_aware_require, input.type_aware_meta),
);
gates.insert_if(GateName::ParseError, input.parse_error.clone());
gates.insert(
GateName::ErrorSeverityFindings,
error_severity_outcome(input.has_error_severity, true),
);
gates.into_option()
}
pub fn dupes_gate_outcomes(
threshold: f64,
duplication_percentage: f64,
baseline_staleness: Option<&fallow_output::BaselineStaleness>,
fail_on_stale_baseline: bool,
) -> Option<GateOutcomes> {
let mut gates = GateOutcomes::new();
gates.insert_if(
GateName::DuplicationThreshold,
duplication_threshold_outcome(threshold, duplication_percentage, true),
);
gates.insert_if(
GateName::StaleBaseline,
stale_baseline_outcome(baseline_staleness, fail_on_stale_baseline),
);
gates.into_option()
}
pub struct HealthGateInputs<'a> {
pub report_only: bool,
pub min_score: Option<(f64, Option<f64>)>,
pub min_severity: Option<(fallow_output::FindingSeverity, usize)>,
pub coverage_gaps: Option<bool>,
pub runtime_coverage: Option<bool>,
pub baseline_staleness: Option<&'a fallow_output::BaselineStaleness>,
pub fail_on_stale_baseline: bool,
pub has_findings: bool,
pub type_aware_meta: Option<&'a fallow_types::envelope::TypeAwareMeta>,
pub parse_error: Option<GateOutcome>,
}
pub fn health_gate_outcomes(input: &HealthGateInputs<'_>) -> Option<GateOutcomes> {
let enforced = !input.report_only;
let mut gates = GateOutcomes::new();
gates.insert_if(
GateName::TypeAwareRequire,
type_aware_meta_outcome(input.type_aware_meta),
);
if let Some((threshold, score)) = input.min_score {
gates.insert(
GateName::HealthMinScore,
score.map_or_else(
|| GateOutcome::new(GateStatus::Skipped, false),
|score| {
GateOutcome::measured(status_of(score < threshold), enforced, score, threshold)
},
),
);
}
if let Some((floor, reached)) = input.min_severity {
gates.insert(
GateName::HealthMinSeverity,
GateOutcome::counted(
status_of(reached > 0),
enforced,
#[expect(
clippy::cast_precision_loss,
reason = "a finding count never approaches the f64 integer limit"
)]
{
reached as f64
},
severity_floor_label(floor),
),
);
}
if let Some(has_gaps) = input.coverage_gaps {
gates.insert(
GateName::HealthCoverageGaps,
GateOutcome::new(status_of(has_gaps), enforced),
);
}
if let Some(failing) = input.runtime_coverage {
gates.insert(
GateName::HealthRuntimeCoverage,
GateOutcome::new(status_of(failing), enforced),
);
}
gates.insert_if(
GateName::StaleBaseline,
stale_baseline_outcome(
input.baseline_staleness,
input.fail_on_stale_baseline && enforced,
),
);
gates.insert_if(
GateName::ParseError,
input.parse_error.clone().map(|mut outcome| {
outcome.enforced &= enforced;
outcome
}),
);
if input.min_severity.is_none() {
gates.insert(
GateName::HealthFindings,
if input.min_score.is_some() {
GateOutcome::new(GateStatus::Skipped, false)
} else {
health_findings_outcome(input.has_findings, enforced)
},
);
}
gates.into_option()
}
const fn severity_floor_label(severity: fallow_output::FindingSeverity) -> &'static str {
match severity {
fallow_output::FindingSeverity::Moderate => "moderate",
fallow_output::FindingSeverity::High => "high",
fallow_output::FindingSeverity::Critical => "critical",
}
}
pub fn security_gate_outcomes(
gate_failed: Option<bool>,
advisory_failed: bool,
advisory_enforced: bool,
) -> Option<GateOutcomes> {
let mut gates = GateOutcomes::new();
if let Some(gate_failed) = gate_failed {
gates.insert(
GateName::Security,
GateOutcome::new(status_of(gate_failed), true),
);
gates.insert(
GateName::SecurityAdvisory,
GateOutcome::new(GateStatus::Skipped, false),
);
return gates.into_option();
}
gates.insert(
GateName::SecurityAdvisory,
GateOutcome::new(status_of(advisory_failed), advisory_enforced),
);
gates.into_option()
}
pub fn audit_gate_outcomes(
verdict: GateStatus,
loaded_any_baseline: bool,
type_aware_meta: Option<&fallow_types::envelope::TypeAwareMeta>,
parse_error: Option<GateOutcome>,
) -> Option<GateOutcomes> {
let mut gates = GateOutcomes::new();
gates.insert(GateName::AuditVerdict, GateOutcome::new(verdict, true));
gates.insert_if(
GateName::TypeAwareRequire,
type_aware_meta_outcome(type_aware_meta),
);
gates.insert_if(GateName::ParseError, parse_error);
if loaded_any_baseline {
gates.insert(
GateName::StaleBaseline,
GateOutcome::new(GateStatus::Skipped, false),
);
}
gates.into_option()
}
pub struct CombinedGateInputs<'a> {
pub regression: Option<&'a crate::regression::RegressionOutcome>,
pub baselines: [Option<fallow_output::BaselineStaleness>; 3],
pub fail_on_stale_baseline: bool,
pub type_aware_failed: Option<bool>,
pub duplication: Option<(f64, f64)>,
pub has_error_severity: Option<bool>,
pub health_has_findings: Option<bool>,
pub parse_error: Option<GateOutcome>,
}
pub fn combined_gate_outcomes(input: &CombinedGateInputs<'_>) -> Option<GateOutcomes> {
let mut gates = GateOutcomes::new();
gates.insert_if(
GateName::Regression,
regression_outcome(input.regression, true),
);
gates.insert_if(
GateName::StaleBaseline,
merged_stale_baseline_outcome(&input.baselines, input.fail_on_stale_baseline),
);
if let Some(failed) = input.type_aware_failed {
gates.insert(
GateName::TypeAwareRequire,
GateOutcome::new(status_of(failed), true),
);
}
gates.insert_if(GateName::ParseError, input.parse_error.clone());
if let Some((threshold, percentage)) = input.duplication {
gates.insert_if(
GateName::DuplicationThreshold,
duplication_threshold_outcome(threshold, percentage, false),
);
}
if let Some(has_error_severity) = input.has_error_severity {
gates.insert(
GateName::ErrorSeverityFindings,
error_severity_outcome(has_error_severity, false),
);
}
if let Some(has_findings) = input.health_has_findings {
gates.insert(
GateName::HealthFindings,
health_findings_outcome(has_findings, false),
);
}
gates.into_option()
}
fn merged_stale_baseline_outcome(
baselines: &[Option<fallow_output::BaselineStaleness>],
fail_on_stale_baseline: bool,
) -> Option<GateOutcome> {
let outcomes: Vec<GateOutcome> = baselines
.iter()
.filter_map(|staleness| stale_baseline_outcome(staleness.as_ref(), fail_on_stale_baseline))
.collect();
let judged = |status: GateStatus| {
outcomes
.iter()
.find(|outcome| outcome.status == status)
.cloned()
};
judged(GateStatus::Fail)
.or_else(|| judged(GateStatus::Pass))
.or_else(|| outcomes.first().cloned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_run_that_arms_nothing_still_states_the_default_rule() {
let gates = check_gate_outcomes(&CheckGateInputs {
has_error_severity: true,
regression: None,
baseline_staleness: None,
fail_on_stale_baseline: false,
type_aware_require: fallow_config::TypeAwareRequire::BestEffort,
type_aware_meta: None,
parse_error: None,
})
.expect("the default exit rule is always published");
let outcome = gates
.get(GateName::ErrorSeverityFindings)
.expect("the default exit rule");
assert_eq!(outcome.status, GateStatus::Fail);
assert!(outcome.fails_run());
}
#[test]
fn the_object_always_carries_the_rule_that_decides_the_exit_code() {
let gates = check_gate_outcomes(&CheckGateInputs {
has_error_severity: true,
regression: Some(&crate::regression::RegressionOutcome::Pass {
baseline_total: 1,
current_total: 1,
}),
baseline_staleness: None,
fail_on_stale_baseline: false,
type_aware_require: fallow_config::TypeAwareRequire::BestEffort,
type_aware_meta: None,
parse_error: None,
})
.expect("the regression gate armed the object");
assert_eq!(
gates.get(GateName::Regression).expect("armed").status,
GateStatus::Pass
);
let outcome = gates
.get(GateName::ErrorSeverityFindings)
.expect("the default exit rule joins the object");
assert_eq!(outcome.status, GateStatus::Fail);
assert!(outcome.enforced);
}
#[test]
fn a_published_baseline_verdict_is_unenforced_without_the_flag() {
let staleness = fallow_output::BaselineStaleness {
baseline_entries: 8,
matched_entries: 3,
stale_entries: 5,
current_findings: 0,
change_scoped: false,
stale: false,
warning: fallow_output::BaselineStalenessAdvisory::None,
gate_trips: true,
moved_entries: 0,
unrecognised_format: false,
saved_by: None,
scope_reasons: fallow_output::BaselineScopeReasons::empty(),
};
let unarmed = stale_baseline_outcome(Some(&staleness), false).expect("verdict published");
assert_eq!(unarmed.status, GateStatus::Fail);
assert!(!unarmed.enforced);
assert!(!unarmed.fails_run());
let armed = stale_baseline_outcome(Some(&staleness), true).expect("verdict published");
assert!(armed.fails_run());
}
#[test]
fn a_change_scoped_baseline_run_stands_down() {
let staleness = fallow_output::BaselineStaleness {
baseline_entries: 8,
matched_entries: 0,
stale_entries: 8,
current_findings: 0,
change_scoped: true,
stale: false,
warning: fallow_output::BaselineStalenessAdvisory::None,
gate_trips: false,
moved_entries: 0,
unrecognised_format: false,
saved_by: None,
scope_reasons: fallow_output::BaselineScopeReasons::empty()
.with(fallow_output::ScopeReason::Production),
};
let outcome = stale_baseline_outcome(Some(&staleness), true).expect("verdict published");
assert_eq!(outcome.status, GateStatus::Skipped);
assert!(!outcome.fails_run());
}
#[test]
fn a_zero_threshold_arms_no_duplication_gate() {
assert!(duplication_threshold_outcome(0.0, 100.0, true).is_none());
let outcome = duplication_threshold_outcome(5.0, 100.0, true).expect("gate armed");
assert_eq!(outcome.status, GateStatus::Fail);
assert_eq!(outcome.observed, Some(100.0));
assert_eq!(outcome.threshold, Some(5.0));
}
#[test]
fn a_skipped_regression_comparison_is_not_a_pass() {
let outcome = regression_outcome(
Some(&crate::regression::RegressionOutcome::Skipped {
reason: "changed-since",
}),
true,
)
.expect("comparison ran");
assert_eq!(outcome.status, GateStatus::Skipped);
}
fn type_aware_meta(
required: fallow_types::semantic::SemanticCompletenessRequirement,
completeness: fallow_types::semantic::SemanticCompleteness,
) -> fallow_types::envelope::TypeAwareMeta {
fallow_types::envelope::TypeAwareMeta {
required_completeness: Some(required),
identity: Some(fallow_types::semantic::SemanticAnalysisIdentity {
completeness,
..Default::default()
}),
..Default::default()
}
}
#[test]
fn the_type_aware_entry_follows_the_recorded_policy() {
use fallow_types::semantic::{SemanticCompleteness, SemanticCompletenessRequirement};
assert_eq!(type_aware_meta_outcome(None), None);
let best_effort = type_aware_meta(
SemanticCompletenessRequirement::BestEffort,
SemanticCompleteness::Partial,
);
assert_eq!(type_aware_meta_outcome(Some(&best_effort)), None);
let complete = type_aware_meta(
SemanticCompletenessRequirement::Complete,
SemanticCompleteness::Complete,
);
let partial = type_aware_meta(
SemanticCompletenessRequirement::Complete,
SemanticCompleteness::Partial,
);
for (meta, status) in [(&complete, GateStatus::Pass), (&partial, GateStatus::Fail)] {
let outcome = type_aware_meta_outcome(Some(meta)).expect("the policy arms the gate");
assert_eq!(outcome.status, status);
assert!(outcome.enforced);
assert_eq!(
crate::exit_codes::gate_exit_code(GateName::TypeAwareRequire, outcome.status),
u8::from(crate::report::ci::required_type_aware_incomplete(Some(
meta
))),
"the entry and the exit path read one predicate"
);
}
}
fn degraded(root: &std::path::Path, file: &str, panicked: bool) -> WorkspaceDiagnostic {
WorkspaceDiagnostic::new(
root,
root.join(file),
WorkspaceDiagnosticKind::SourceParseDegraded {
error_count: 2,
panicked,
},
)
}
#[test]
fn only_parse_degraded_files_count_and_they_come_sorted_and_relative() {
let root = std::path::Path::new("/project");
let diagnostics = vec![
degraded(root, "src/z.ts", false),
WorkspaceDiagnostic::new(
root,
root.join("node_modules"),
WorkspaceDiagnosticKind::NodeModulesMissing,
),
degraded(root, "src/a.tsx", true),
degraded(root, "src/a.tsx", true),
];
let files = parse_degraded_files(root, &diagnostics);
assert_eq!(
files,
vec![
GateFile {
path: "src/a.tsx".to_owned(),
error_count: 2,
panicked: true,
},
GateFile {
path: "src/z.ts".to_owned(),
error_count: 2,
panicked: false,
},
]
);
assert_eq!(
parse_error_reason(&files[0]),
"2 parser errors, the parser stopped"
);
}
#[test]
fn an_unarmed_parse_error_gate_publishes_nothing() {
let files = [GateFile {
path: "src/a.ts".to_owned(),
error_count: 1,
panicked: false,
}];
assert_eq!(parse_error_outcome(false, true, &files), None);
let armed = parse_error_outcome(true, true, &files).expect("armed");
assert!(armed.fails_run());
assert_eq!(armed.observed, Some(1.0));
let clean = parse_error_outcome(true, true, &[]).expect("armed");
assert_eq!(clean.status, GateStatus::Pass);
}
#[test]
fn report_only_clamps_the_parse_error_entry() {
let files = vec![GateFile {
path: "src/a.ts".to_owned(),
error_count: 1,
panicked: false,
}];
let gates = health_gate_outcomes(&HealthGateInputs {
report_only: true,
min_score: None,
min_severity: None,
coverage_gaps: None,
runtime_coverage: None,
baseline_staleness: None,
fail_on_stale_baseline: false,
has_findings: false,
type_aware_meta: None,
parse_error: parse_error_outcome(true, true, &files),
})
.expect("health always states its default rule");
let entry = gates.get(GateName::ParseError).expect("armed");
assert_eq!(entry.status, GateStatus::Fail);
assert!(!entry.fails_run());
}
#[test]
fn report_only_keeps_the_type_aware_entry_enforced() {
use fallow_types::semantic::{SemanticCompleteness, SemanticCompletenessRequirement};
let partial = type_aware_meta(
SemanticCompletenessRequirement::Complete,
SemanticCompleteness::Partial,
);
let gates = health_gate_outcomes(&HealthGateInputs {
report_only: true,
min_score: None,
min_severity: None,
coverage_gaps: None,
runtime_coverage: None,
baseline_staleness: None,
fail_on_stale_baseline: false,
has_findings: false,
type_aware_meta: Some(&partial),
parse_error: None,
})
.expect("health always states its default rule");
let json = serde_json::to_value(&gates).expect("gate outcomes serialize");
assert_eq!(
json["type-aware-require"],
serde_json::json!({ "status": "fail", "enforced": true })
);
assert_eq!(json["health-findings"]["enforced"], false);
}
}