1use std::path::PathBuf;
4
5use fallow_output::{HealthGrouping, HealthReport, RootEnvelopeMode};
6use fallow_types::output_format::OutputFormat;
7use fallow_types::workspace::WorkspaceDiagnostic;
8
9mod audit;
10mod dead_code;
11mod decision_surface;
12mod duplication;
13mod feature_flags;
14mod trace;
15
16pub use crate::runtime_output::{
17 AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
18 BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
19 CircularDependenciesProgrammaticOutput, DeadCodeOutput, DeadCodeProgrammaticOutput,
20 DecisionSurfaceProgrammaticOutput, DuplicationOutput, DuplicationProgrammaticOutput,
21 FeatureFlagsOutput, FeatureFlagsProgrammaticOutput, HealthJsonReportInput,
22 HealthProgrammaticOutput, TraceCloneOutput, TraceCloneProgrammaticOutput,
23 TraceDependencyOutput, TraceDependencyProgrammaticOutput, TraceExportOutput,
24 TraceExportProgrammaticOutput, TraceFileOutput, TraceFileProgrammaticOutput,
25 serialize_health_report_json,
26};
27pub use audit::run_audit;
28pub use dead_code::{run_boundary_violations, run_circular_dependencies, run_dead_code};
29pub use decision_surface::run_decision_surface;
30pub use duplication::run_duplication;
31pub use feature_flags::run_feature_flags;
32pub use trace::{run_trace_clone, run_trace_dependency, run_trace_export, run_trace_file};
33
34use crate::{
35 ComplexityOptions, ProgrammaticError,
36 analysis_context::{ProgrammaticAnalysisContext, resolve_programmatic_analysis_context},
37 next_steps::{setup_pointer_applicable, suggestions_enabled},
38};
39
40type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
41
42pub struct ProgrammaticHealthNextStepFacts {
47 pub suggestions_enabled: bool,
48 pub offer_setup: bool,
49 pub impact_digest: Option<fallow_output::ImpactDigestCounts>,
50 pub audit_changed: bool,
51}
52
53pub struct ProgrammaticHealthAnalysis {
58 pub report: HealthReport,
59 pub grouping: Option<HealthGrouping>,
60 pub root: PathBuf,
61 pub elapsed: std::time::Duration,
62}
63
64impl ProgrammaticHealthAnalysis {
65 fn from_engine<GroupResolver>(
66 analysis: fallow_engine::HealthAnalysisResult<GroupResolver>,
67 ) -> Self {
68 Self {
69 root: analysis.config.root,
70 report: analysis.report,
71 grouping: analysis.grouping,
72 elapsed: analysis.elapsed,
73 }
74 }
75}
76
77pub struct ProgrammaticHealthRun {
82 pub analysis: ProgrammaticHealthAnalysis,
83 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
84 pub next_step_facts: ProgrammaticHealthNextStepFacts,
85 pub telemetry_analysis_run_id: Option<String>,
86}
87
88pub trait ProgrammaticHealthRunner {
93 fn run_programmatic_health(
100 &self,
101 options: &ComplexityOptions,
102 ) -> Result<ProgrammaticHealthRun, ProgrammaticError>;
103}
104
105#[derive(Debug, Clone, Copy, Default)]
114pub struct EngineHealthRunner;
115
116impl ProgrammaticHealthRunner for EngineHealthRunner {
117 fn run_programmatic_health(
118 &self,
119 options: &ComplexityOptions,
120 ) -> Result<ProgrammaticHealthRun, ProgrammaticError> {
121 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
122 resolved.install(|| run_programmatic_health_on_engine(&resolved, options))
123 }
124}
125
126fn run_programmatic_health_on_engine(
127 resolved: &ProgrammaticAnalysisContext,
128 options: &ComplexityOptions,
129) -> ProgrammaticResult<ProgrammaticHealthRun> {
130 let health_options = derive_programmatic_health_execution_options(resolved, options);
131 let result =
132 fallow_engine::run_ungrouped_health(&health_options, resolved.workspace_roots.clone())
133 .map_err(|_| generic_health_error("health"))?;
134
135 let root = result.config.root.clone();
136 let next_step_facts = ProgrammaticHealthNextStepFacts {
137 suggestions_enabled: suggestions_enabled(),
138 offer_setup: setup_pointer_applicable(&root),
139 impact_digest: None,
140 audit_changed: fallow_engine::is_git_repo(&root),
141 };
142 Ok(ProgrammaticHealthRun {
143 workspace_diagnostics: result.workspace_diagnostics.clone(),
144 analysis: ProgrammaticHealthAnalysis::from_engine(result.without_group_resolver()),
145 next_step_facts,
146 telemetry_analysis_run_id: None,
147 })
148}
149
150fn generic_health_error(command: &str) -> ProgrammaticError {
151 let code = format!(
152 "FALLOW_{}_FAILED",
153 command.replace('-', "_").to_ascii_uppercase()
154 );
155 ProgrammaticError::new(format!("{command} failed"), 2)
156 .with_code(code)
157 .with_context(format!("fallow {command}"))
158 .with_help(format!(
159 "Re-run `fallow {command} --format json --quiet` in the target project for CLI diagnostics"
160 ))
161}
162
163pub fn run_health(options: &ComplexityOptions) -> ProgrammaticResult<HealthProgrammaticOutput> {
170 run_health_with_runner(options, &EngineHealthRunner)
171}
172
173#[must_use]
174fn derive_programmatic_health_execution_options<'a>(
175 resolved: &'a ProgrammaticAnalysisContext,
176 options: &'a ComplexityOptions,
177) -> fallow_engine::HealthExecutionOptions<'a> {
178 let run = crate::derive_complexity_run_options(options);
179
180 fallow_engine::HealthExecutionOptions {
181 root: resolved.root(),
182 config_path: resolved.config_path(),
183 output: OutputFormat::Human,
184 no_cache: resolved.no_cache(),
185 threads: resolved.threads(),
186 quiet: true,
187 complexity_breakdown: run.complexity_breakdown,
188 thresholds: crate::thresholds_to_engine(run.thresholds),
189 top: run.top,
190 sort: crate::complexity_sort_to_engine(run.sort),
191 production: resolved.production_override().unwrap_or(false),
192 production_override: resolved.production_override(),
193 changed_since: resolved.changed_since(),
194 diff_index: resolved.diff_index(),
195 use_shared_diff_index: false,
196 workspace: resolved.workspace(),
197 changed_workspaces: resolved.changed_workspaces(),
198 baseline: None,
199 save_baseline: None,
200 complexity: run.sections.complexity,
201 file_scores: run.sections.file_scores,
202 coverage_gaps: run.sections.coverage_gaps,
203 config_activates_coverage_gaps: !run.sections.any_section,
204 hotspots: run.sections.hotspots,
205 ownership: run.sections.ownership,
206 targets: run.sections.targets,
207 css: run.css,
208 force_full: run.sections.force_full,
209 score_only_output: run.sections.score_only_output,
210 enforce_coverage_gap_gate: true,
211 effort: run.effort.map(crate::target_effort_to_output),
212 score: run.sections.score,
213 gates: fallow_engine::HealthGateOptions::default(),
214 since: run.since,
215 min_commits: run.min_commits,
216 explain: resolved.explain_enabled(),
217 summary: false,
218 save_snapshot: None,
219 trend: false,
220 coverage_inputs: crate::coverage_inputs_to_engine(run.coverage_inputs),
221 performance: false,
222 runtime_coverage: None,
223 churn_file: None,
224 group_by: None,
225 ownership_emails: run
226 .ownership_emails
227 .map(crate::ownership_email_mode_to_config),
228 }
229}
230
231pub fn run_complexity_with_runner(
243 options: &ComplexityOptions,
244 runner: &impl ProgrammaticHealthRunner,
245) -> ProgrammaticResult<HealthProgrammaticOutput> {
246 crate::validate_complexity_options(options)?;
247 let ProgrammaticHealthRun {
248 analysis,
249 workspace_diagnostics,
250 next_step_facts,
251 telemetry_analysis_run_id,
252 } = runner.run_programmatic_health(options)?;
253 let root = analysis.root.clone();
254 let next_steps =
255 fallow_output::build_health_next_steps(fallow_output::build_health_next_steps_input(
256 &analysis.report,
257 next_step_facts.suggestions_enabled,
258 next_step_facts.offer_setup,
259 next_step_facts.impact_digest,
260 next_step_facts.audit_changed,
261 ));
262 Ok(HealthProgrammaticOutput {
263 report: analysis.report,
264 grouping: analysis.grouping,
265 root,
266 elapsed: analysis.elapsed,
267 explain: options.analysis.explain,
268 workspace_diagnostics,
269 next_steps,
270 envelope_mode: root_envelope_mode(),
271 telemetry_analysis_run_id,
272 })
273}
274
275pub fn run_health_with_runner(
281 options: &ComplexityOptions,
282 runner: &impl ProgrammaticHealthRunner,
283) -> ProgrammaticResult<HealthProgrammaticOutput> {
284 run_complexity_with_runner(options, runner)
285}
286
287const fn root_envelope_mode() -> RootEnvelopeMode {
288 RootEnvelopeMode::Tagged
289}
290
291#[cfg(test)]
292mod tests;