use std::path::Path;
use std::process::ExitCode;
use fallow_config::{OutputFormat, TypeAwareRequire};
use fallow_types::envelope::TypeAwareMeta;
pub fn degraded_message(root: &Path, error: &str) -> String {
format!(
"type-aware refinement unavailable for {}: {error}; showing conservative syntactic findings",
root.display()
)
}
pub fn degraded_meta(message: String, require: TypeAwareRequire) -> TypeAwareMeta {
TypeAwareMeta {
required_completeness: Some(require.into()),
executed: false,
warning_count: 1,
warnings: vec![message],
..TypeAwareMeta::default()
}
}
pub struct DegradeContext<'a> {
pub root: &'a Path,
pub error: &'a str,
pub failure_label: &'a str,
pub require: TypeAwareRequire,
pub quiet: bool,
pub output: OutputFormat,
}
pub fn degrade_or_fail(ctx: &DegradeContext<'_>) -> Result<TypeAwareMeta, ExitCode> {
if ctx.require == TypeAwareRequire::Complete {
return Err(crate::error::emit_error(
&format!("{}: {}", ctx.failure_label, ctx.error),
2,
ctx.output,
));
}
let message = degraded_message(ctx.root, ctx.error);
emit_warning(&message, ctx.quiet);
Ok(degraded_meta(message, ctx.require))
}
#[expect(
clippy::print_stderr,
reason = "degradation notice belongs on stderr next to other CLI warnings"
)]
fn emit_warning(message: &str, quiet: bool) {
if !quiet {
eprintln!("Warning: {message}");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context(error: &str, require: TypeAwareRequire) -> DegradeContext<'_> {
DegradeContext {
root: Path::new("/repo"),
error,
failure_label: "Type-aware analysis failed",
require,
quiet: true,
output: OutputFormat::Json,
}
}
#[test]
fn best_effort_degrades_with_a_recorded_warning() {
let meta = degrade_or_fail(&context("sidecar timed out", TypeAwareRequire::BestEffort))
.expect("best-effort should continue with syntactic findings");
assert!(!meta.executed);
assert_eq!(meta.warning_count, 1);
assert!(meta.warnings[0].contains("sidecar timed out"));
assert!(
meta.warnings[0].contains("showing conservative syntactic findings"),
"warning should explain the fallback: {}",
meta.warnings[0]
);
}
#[test]
fn complete_still_fails_hard() {
let outcome = degrade_or_fail(&context("sidecar timed out", TypeAwareRequire::Complete));
assert!(outcome.is_err(), "require=complete must not degrade");
}
}