use super::*;
pub(crate) fn rule_timings(state: &CheckpointState) -> (Vec<(&str, f64)>, f64) {
let mut timings: Vec<(&str, f64)> = state
.benchmarks
.iter()
.filter(|(_, b)| b.recorded_as.is_none())
.map(|(rule, bench)| (rule.as_str(), bench.wall_time_secs))
.collect();
timings.sort_by(|a, b| b.1.total_cmp(&a.1));
let total = timings.iter().map(|(_, secs)| secs).sum();
(timings, total)
}
pub(crate) fn print_target_skipped_note(
skipped: &str,
when_false_rules: &std::collections::HashSet<String>,
run_log: Option<&SharedRunLog>,
) {
let detail = if when_false_rules.contains(skipped) {
format!(
"target '{skipped}' is when-gated false — it never runs; \
removed from the execution set (its upstream was pruned too)"
)
} else {
format!(
"target '{skipped}' is unreachable — its upstream is when-gated \
false; removed from the execution set"
)
};
diagnostic_narrate(format_args!("{} {detail}", "Note:".yellow()), run_log);
}
pub(crate) fn print_when_pruned_note(rule_name: &str, run_log: Option<&SharedRunLog>) {
let detail = format!(
"rule '{rule_name}' is unreachable — its upstream is when-gated \
false; removed from the full-run execution set"
);
diagnostic_narrate(format_args!("{} {detail}", "Note:".yellow()), run_log);
}
pub(crate) fn run_workdir_check(workdir: Option<&std::path::Path>, path: &str) -> bool {
let p = std::path::Path::new(path);
if p.is_absolute() {
p.exists()
} else {
workdir
.unwrap_or_else(|| std::path::Path::new("."))
.join(p)
.exists()
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn compute_staleness_reasons(
state: &CheckpointState,
checkpoint_path: &std::path::Path,
) -> std::collections::BTreeMap<String, (String, String)> {
use crate::commands::run_preview::RuleStatus;
let mut reasons = std::collections::BTreeMap::new();
if state.completed_rules.is_empty() {
return reasons;
}
let Some(workflow_path) = state.workflow_path.as_ref() else {
return reasons;
};
let workflow = std::path::PathBuf::from(workflow_path);
if !workflow.exists() {
return reasons;
}
let Ok(config) = WorkflowConfig::from_file(&workflow) else {
return reasons;
};
let workflow_dir = oxo_flow_core::parent_dir(&workflow).to_path_buf();
let Ok(dag) = WorkflowDag::from_rules_with_config(
&config.rules,
&config_placeholder_values(&config.config),
) else {
return reasons;
};
let Ok(raw_order) = dag.execution_order() else {
return reasons;
};
let order_set: std::collections::HashSet<String> = raw_order.into_iter().collect();
let mut order: Vec<String> = Vec::new();
let Ok(groups) = dag.parallel_groups() else {
return reasons;
};
for group in groups {
let mut level: Vec<String> = group
.into_iter()
.filter(|n| order_set.contains(n))
.collect();
level.sort();
order.extend(level);
}
let wildcard_values = config_placeholder_values(&config.config);
let sensitive_keys: std::collections::HashSet<String> = config
.config_meta
.iter()
.filter(|(_, def)| def.sensitive)
.map(|(key, _)| key.clone())
.collect();
let interpreter_map = config.workflow.interpreter_map.clone();
let mut adopted = state.clone();
crate::commands::run::adopt_checkpoint_for_workflow(&mut adopted, &workflow, None);
let preview = crate::commands::run_preview::preview_run_plan(
&adopted,
&config,
&dag,
&order,
&workflow_dir,
&wildcard_values,
&sensitive_keys,
&interpreter_map,
checkpoint_path,
false,
false,
);
for entry in &preview.plan {
if !state.completed_rules.contains(&entry.name) {
continue;
}
let (reason, detail) = match entry.status {
RuleStatus::Skipped | RuleStatus::SkippedByWhen | RuleStatus::SkippedFresh => {
continue;
}
RuleStatus::NeverCompleted if !adopted.completed_rules.contains(&entry.name) => (
"checkpoint reset".to_string(),
"completed credit was invalidated (foreign workflow adoption)".to_string(),
),
RuleStatus::NeverCompleted => (
"output missing".to_string(),
"completion record present but the executor would re-run — outputs are gone or failed cleanup removed them".to_string(),
),
RuleStatus::ConfigInvalidated => (
"config changed".to_string(),
"an effective config value differs from the recorded snapshot".to_string(),
),
RuleStatus::InputInvalidated => (
"input changed".to_string(),
"recorded input manifest (paths/size/mtime) no longer matches disk".to_string(),
),
RuleStatus::OutputsMissing => (
"output missing".to_string(),
"recorded output is gone from the workdir".to_string(),
),
RuleStatus::Cascaded { from: ref f1 } => (
"cascaded".to_string(),
format!("upstream rule '{f1}' will re-run"),
),
RuleStatus::CascadedUpstream { from: ref f2 } => (
"missing input".to_string(),
format!(
"a downstream rule needs an intermediate that is gone; producer '{f2}' re-runs first"
),
),
RuleStatus::Forced => ("forced".to_string(), "--rerun was requested".to_string()),
};
reasons.insert(entry.name.clone(), (reason, detail));
}
reasons
}
pub(crate) fn reconcile_failed_rules<'a>(
failed_rules: &'a std::collections::BTreeSet<String>,
when_verdicts: &'a std::collections::BTreeMap<String, bool>,
) -> (Vec<&'a str>, Vec<&'a str>) {
let mut real_failed = Vec::new();
let mut skipped_when_false = Vec::new();
for rule in failed_rules {
if when_verdicts.get(rule) == Some(&false) {
skipped_when_false.push(rule.as_str());
} else {
real_failed.push(rule.as_str());
}
}
(real_failed, skipped_when_false)
}