Skip to main content

fallow_api/
runtime_output.rs

1//! Typed programmatic runtime outputs and shared output-contract serializers.
2
3use std::path::{Path, PathBuf};
4
5pub use fallow_output::HEALTH_SCHEMA_VERSION;
6use fallow_output::{
7    CheckOutput, DupesOutput, FeatureFlagFinding, FeatureFlagsOutput as FeatureFlagsOutputContract,
8    GroupByMode, HealthGroup, HealthGrouping, HealthJsonOutputInput, HealthOutputInput,
9    HealthReport, health_meta,
10};
11use fallow_types::output::NextStep;
12use fallow_types::output_dead_code::{
13    BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
14    CircularDependencyFinding,
15};
16use fallow_types::results::AnalysisResults;
17use fallow_types::workspace::WorkspaceDiagnostic;
18use rustc_hash::FxHashSet;
19
20use crate::{AuditAttribution, AuditSummary, AuditVerdict};
21use crate::{CloneFamilyFinding, CloneGroupFinding, DupesReportPayload, DuplicationGroup};
22
23/// Concrete dead-code output contract returned by typed programmatic runs.
24pub type DeadCodeOutput = CheckOutput;
25
26/// Concrete circular-dependency output contract returned by typed runs.
27pub type CircularDependenciesOutput = CheckOutput;
28
29/// Concrete boundary-family output contract returned by typed runs.
30pub type BoundaryViolationsOutput = CheckOutput;
31
32/// Concrete duplication output contract returned by typed programmatic runs.
33pub type DuplicationOutput = DupesOutput<DupesReportPayload, DuplicationGroup>;
34
35/// Concrete feature-flag output contract returned by typed programmatic runs.
36pub type FeatureFlagsOutput = FeatureFlagsOutputContract;
37
38/// Concrete export trace output returned by typed programmatic runs.
39pub type TraceExportOutput = fallow_types::trace::ExportTrace;
40
41/// Concrete class / enum / store member trace output (the `trace_export`
42/// fallback when the name is a member rather than a top-level export). See
43/// issue #1744.
44pub type TraceClassMemberOutput = fallow_types::trace::ClassMemberTrace;
45
46/// The `trace_export` target: either a top-level export or (fallback) a class /
47/// enum / store member declared on one. Serialized untagged so the export shape
48/// stays byte-identical to the historical contract and the member shape matches
49/// the CLI's member trace; consumers distinguish by the presence of
50/// `export_name` (export) vs `member_name` / `owner_export` (member).
51#[derive(Debug, serde::Serialize)]
52#[serde(untagged)]
53pub enum TraceExportTargetOutput {
54    /// A top-level export trace.
55    Export(TraceExportOutput),
56    /// A class / enum / store member trace.
57    Member(TraceClassMemberOutput),
58}
59
60/// Concrete file trace output returned by typed programmatic runs.
61pub type TraceFileOutput = fallow_types::trace::FileTrace;
62
63/// Concrete dependency trace output returned by typed programmatic runs.
64pub type TraceDependencyOutput = fallow_types::trace::DependencyTrace;
65
66/// Concrete import-path trace output returned by typed programmatic runs.
67pub type TraceImportPathOutput = fallow_types::trace::ImportPathTrace;
68
69/// Concrete duplicate-code trace output returned by typed programmatic runs.
70pub type TraceCloneOutput = fallow_types::trace::CloneTrace;
71
72/// Concrete stack-trace resolution returned by typed programmatic runs.
73pub type TraceErrorOutput = fallow_types::trace_error::ErrorTrace;
74
75/// Inputs for serializing health JSON output through the API boundary.
76pub struct HealthJsonReportInput<'a> {
77    /// Every gate this run evaluated, absent when it evaluated none. The
78    /// programmatic route runs no CLI-layer gate and leaves this `None`.
79    pub gate_outcomes: Option<fallow_output::GateOutcomes>,
80    /// Every narrowing or shaping request this run received, absent when it
81    /// was asked for nothing. The programmatic route resolves no CLI flag and
82    /// leaves this `None`; an entry whose `status` is not `applied` means the
83    /// report is wider than what was asked for.
84    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
85    /// Typed health report to serialize.
86    pub report: HealthReport,
87    /// Project root; its prefix is stripped from every path in the output.
88    pub root: &'a Path,
89    /// Analysis wall time, emitted as `elapsed_ms`.
90    pub elapsed: std::time::Duration,
91    /// Emit explain metadata under `meta`.
92    pub explain: bool,
93    /// Type-aware pass metadata merged into `meta` even when `explain` is
94    /// off.
95    pub type_aware: Option<fallow_types::envelope::TypeAwareMeta>,
96    /// Grouping axis recorded as `grouped_by`; `None` when ungrouped.
97    pub grouped_by: Option<GroupByMode>,
98    /// Precomputed group buckets matching `grouped_by`.
99    pub groups: Option<Vec<HealthGroup>>,
100    /// Non-fatal per-file diagnostics collected during the workspace walk.
101    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
102    /// Suggested follow-up commands for the consumer.
103    pub next_steps: Vec<NextStep>,
104    /// Analysis run id stamped into telemetry metadata when present.
105    pub telemetry_analysis_run_id: Option<&'a str>,
106}
107
108/// Typed programmatic combined output before JSON serialization.
109#[derive(Debug, Clone)]
110pub struct CombinedProgrammaticOutput {
111    /// Dead-code section; `None` when the run skipped it.
112    pub dead_code: Option<DeadCodeProgrammaticOutput>,
113    /// Duplication section; `None` when the run skipped it.
114    pub duplication: Option<DuplicationProgrammaticOutput>,
115    /// Health section; `None` when the run skipped it.
116    pub health: Option<HealthProgrammaticOutput>,
117    /// Project root used when serializing stable JSON paths.
118    pub root: PathBuf,
119    /// Total wall time across sections, emitted as the root `elapsed_ms`.
120    pub elapsed: std::time::Duration,
121    /// Emit per-section explain metadata when serialized.
122    pub explain: bool,
123    /// Suggested follow-up commands for the consumer.
124    pub next_steps: Vec<NextStep>,
125    /// Analysis run id stamped into telemetry metadata when present.
126    pub telemetry_analysis_run_id: Option<String>,
127    /// What became of the narrowing requests the run received, emitted at the
128    /// envelope root as `request_outcomes`, the only carrier on the combined
129    /// envelope.
130    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
131}
132
133/// Typed programmatic dead-code output before JSON serialization.
134///
135/// This is the API boundary embedders should use when they need access to the
136/// typed engine/output result. Protocol surfaces serialize it explicitly at
137/// their JSON boundary.
138#[derive(Debug, Clone)]
139pub struct DeadCodeProgrammaticOutput {
140    /// Typed dead-code envelope produced by the run.
141    pub output: DeadCodeOutput,
142    /// Project root used when serializing stable JSON paths.
143    pub root: PathBuf,
144    /// Whether duplicate-export findings can be auto-fixed through config;
145    /// propagated onto their fix actions when serialized.
146    pub config_fixable: bool,
147    /// Analysis run id stamped into telemetry metadata when present.
148    pub telemetry_analysis_run_id: Option<String>,
149}
150
151impl DeadCodeProgrammaticOutput {
152    /// Full typed dead-code issue arrays retained by this run.
153    #[must_use]
154    pub fn results(&self) -> &AnalysisResults {
155        &self.output.results
156    }
157
158    /// Project-relative root used when serializing stable JSON paths.
159    #[must_use]
160    pub fn root(&self) -> &Path {
161        &self.root
162    }
163}
164
165/// Typed programmatic circular-dependency output before JSON serialization.
166///
167/// The wire envelope stays the dead-code/check contract, but the Rust API
168/// surface is family-specific so embedders do not have to treat this as a
169/// generic dead-code run.
170#[derive(Debug, Clone)]
171pub struct CircularDependenciesProgrammaticOutput {
172    /// Typed check envelope scoped to circular-dependency findings.
173    pub output: CircularDependenciesOutput,
174    /// Project root used when serializing stable JSON paths.
175    pub root: PathBuf,
176    /// Analysis run id stamped into telemetry metadata when present.
177    pub telemetry_analysis_run_id: Option<String>,
178}
179
180impl CircularDependenciesProgrammaticOutput {
181    /// Full typed issue arrays retained by this family run.
182    #[must_use]
183    pub fn results(&self) -> &AnalysisResults {
184        &self.output.results
185    }
186
187    /// The circular dependency findings retained by this family run.
188    #[must_use]
189    pub fn circular_dependencies(&self) -> &[CircularDependencyFinding] {
190        &self.output.results.circular_dependencies
191    }
192}
193
194impl From<DeadCodeProgrammaticOutput> for CircularDependenciesProgrammaticOutput {
195    fn from(value: DeadCodeProgrammaticOutput) -> Self {
196        Self {
197            output: value.output,
198            root: value.root,
199            telemetry_analysis_run_id: value.telemetry_analysis_run_id,
200        }
201    }
202}
203
204/// Typed programmatic boundary-family output before JSON serialization.
205///
206/// This covers banned imports, boundary coverage, and forbidden call findings
207/// while preserving the stable dead-code/check JSON envelope.
208#[derive(Debug, Clone)]
209pub struct BoundaryViolationsProgrammaticOutput {
210    /// Typed check envelope scoped to boundary-family findings.
211    pub output: BoundaryViolationsOutput,
212    /// Project root used when serializing stable JSON paths.
213    pub root: PathBuf,
214    /// Analysis run id stamped into telemetry metadata when present.
215    pub telemetry_analysis_run_id: Option<String>,
216}
217
218impl BoundaryViolationsProgrammaticOutput {
219    /// Full typed issue arrays retained by this family run.
220    #[must_use]
221    pub fn results(&self) -> &AnalysisResults {
222        &self.output.results
223    }
224
225    /// Banned import boundary findings retained by this family run.
226    #[must_use]
227    pub fn boundary_violations(&self) -> &[BoundaryViolationFinding] {
228        &self.output.results.boundary_violations
229    }
230
231    /// Boundary coverage findings retained by this family run.
232    #[must_use]
233    pub fn boundary_coverage_violations(&self) -> &[BoundaryCoverageViolationFinding] {
234        &self.output.results.boundary_coverage_violations
235    }
236
237    /// Forbidden call findings retained by this family run.
238    #[must_use]
239    pub fn boundary_call_violations(&self) -> &[BoundaryCallViolationFinding] {
240        &self.output.results.boundary_call_violations
241    }
242}
243
244impl From<DeadCodeProgrammaticOutput> for BoundaryViolationsProgrammaticOutput {
245    fn from(value: DeadCodeProgrammaticOutput) -> Self {
246        Self {
247            output: value.output,
248            root: value.root,
249            telemetry_analysis_run_id: value.telemetry_analysis_run_id,
250        }
251    }
252}
253
254/// Typed programmatic duplication output before JSON serialization.
255#[derive(Debug, Clone)]
256pub struct DuplicationProgrammaticOutput {
257    /// Typed duplication envelope produced by the run.
258    pub output: DuplicationOutput,
259    /// Project root used when serializing stable JSON paths.
260    pub root: PathBuf,
261    /// Maximum allowed duplication percentage from the resolved config;
262    /// 0 disables the percentage gate.
263    pub threshold: f64,
264    /// Analysis run id stamped into telemetry metadata when present.
265    pub telemetry_analysis_run_id: Option<String>,
266}
267
268impl DuplicationProgrammaticOutput {
269    /// Typed duplication report payload retained by this run.
270    #[must_use]
271    pub const fn report(&self) -> &DupesReportPayload {
272        &self.output.report
273    }
274
275    /// Clone groups retained by this run, with typed actions and fingerprints.
276    #[must_use]
277    pub fn clone_groups(&self) -> &[CloneGroupFinding] {
278        &self.output.report.clone_groups
279    }
280
281    /// Clone families retained by this run, with nested typed clone groups.
282    #[must_use]
283    pub fn clone_families(&self) -> &[CloneFamilyFinding] {
284        &self.output.report.clone_families
285    }
286
287    /// Grouped duplication buckets when a grouping mode was used.
288    #[must_use]
289    pub fn groups(&self) -> Option<&[DuplicationGroup]> {
290        self.output.groups.as_deref()
291    }
292}
293
294/// Typed programmatic feature-flag output before JSON serialization.
295#[derive(Debug, Clone)]
296pub struct FeatureFlagsProgrammaticOutput {
297    /// Typed feature-flag envelope produced by the run.
298    pub output: FeatureFlagsOutput,
299    /// Analysis run id stamped into telemetry metadata when present.
300    pub telemetry_analysis_run_id: Option<String>,
301}
302
303impl FeatureFlagsProgrammaticOutput {
304    /// Feature flag findings retained by this run.
305    #[must_use]
306    pub fn feature_flags(&self) -> &[FeatureFlagFinding] {
307        &self.output.feature_flags
308    }
309
310    /// Number of feature flags retained by this run after scoping and limits.
311    #[must_use]
312    pub const fn total_flags(&self) -> usize {
313        self.output.total_flags
314    }
315}
316
317/// Typed programmatic export-trace output before JSON serialization. Carries
318/// either an export trace or (fallback) a class / enum / store member trace.
319#[derive(Debug)]
320pub struct TraceExportProgrammaticOutput {
321    /// Typed export-or-member trace produced by the run.
322    pub output: TraceExportTargetOutput,
323}
324
325impl TraceExportProgrammaticOutput {
326    /// Typed export-or-member trace retained by this run.
327    #[must_use]
328    pub const fn trace(&self) -> &TraceExportTargetOutput {
329        &self.output
330    }
331
332    /// The export trace, when the target resolved to a top-level export.
333    #[must_use]
334    pub const fn as_export(&self) -> Option<&TraceExportOutput> {
335        match &self.output {
336            TraceExportTargetOutput::Export(export) => Some(export),
337            TraceExportTargetOutput::Member(_) => None,
338        }
339    }
340
341    /// The member trace, when the target resolved to a class / enum / store
342    /// member (the `trace_export` fallback, issue #1744).
343    #[must_use]
344    pub const fn as_member(&self) -> Option<&TraceClassMemberOutput> {
345        match &self.output {
346            TraceExportTargetOutput::Member(member) => Some(member),
347            TraceExportTargetOutput::Export(_) => None,
348        }
349    }
350}
351
352/// Typed programmatic file-trace output before JSON serialization.
353#[derive(Debug)]
354pub struct TraceFileProgrammaticOutput {
355    /// Typed file trace produced by the run.
356    pub output: TraceFileOutput,
357}
358
359impl TraceFileProgrammaticOutput {
360    /// Typed file trace retained by this run.
361    #[must_use]
362    pub const fn trace(&self) -> &TraceFileOutput {
363        &self.output
364    }
365}
366
367/// Typed programmatic import-path-trace output before JSON serialization.
368#[derive(Debug)]
369pub struct TraceImportPathProgrammaticOutput {
370    /// Typed import-path trace produced by the run.
371    pub output: TraceImportPathOutput,
372}
373
374impl TraceImportPathProgrammaticOutput {
375    /// Typed import-path trace retained by this run.
376    #[must_use]
377    pub const fn trace(&self) -> &TraceImportPathOutput {
378        &self.output
379    }
380}
381
382/// Typed programmatic dependency-trace output before JSON serialization.
383#[derive(Debug)]
384pub struct TraceDependencyProgrammaticOutput {
385    /// Typed dependency trace produced by the run.
386    pub output: TraceDependencyOutput,
387}
388
389impl TraceDependencyProgrammaticOutput {
390    /// Typed dependency trace retained by this run.
391    #[must_use]
392    pub const fn trace(&self) -> &TraceDependencyOutput {
393        &self.output
394    }
395}
396
397/// Typed programmatic stack-trace resolution before JSON serialization.
398#[derive(Debug)]
399pub struct TraceErrorProgrammaticOutput {
400    /// Typed stack-trace resolution produced by the run.
401    pub output: TraceErrorOutput,
402}
403
404impl TraceErrorProgrammaticOutput {
405    /// Typed stack-trace resolution retained by this run.
406    #[must_use]
407    pub const fn trace(&self) -> &TraceErrorOutput {
408        &self.output
409    }
410}
411
412/// Typed programmatic duplicate-code trace output before JSON serialization.
413#[derive(Debug)]
414pub struct TraceCloneProgrammaticOutput {
415    /// Typed clone trace produced by the run.
416    pub output: TraceCloneOutput,
417}
418
419impl TraceCloneProgrammaticOutput {
420    /// Typed clone trace retained by this run.
421    #[must_use]
422    pub const fn trace(&self) -> &TraceCloneOutput {
423        &self.output
424    }
425}
426
427/// Typed programmatic health / complexity output before JSON serialization.
428#[derive(Debug, Clone)]
429pub struct HealthProgrammaticOutput {
430    /// Typed health report produced by the run.
431    pub report: HealthReport,
432    /// Grouped findings when a grouping mode was requested.
433    pub grouping: Option<HealthGrouping>,
434    /// Project root used when serializing stable JSON paths.
435    pub root: PathBuf,
436    /// Analysis wall time, emitted as `elapsed_ms` when serialized.
437    pub elapsed: std::time::Duration,
438    /// Emit explain metadata when serialized.
439    pub explain: bool,
440    /// Non-fatal per-file diagnostics collected during the workspace walk.
441    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
442    /// Suggested follow-up commands for the consumer.
443    pub next_steps: Vec<NextStep>,
444    /// Analysis run id stamped into telemetry metadata when present.
445    pub telemetry_analysis_run_id: Option<String>,
446    /// What became of the narrowing requests the run received, emitted as the
447    /// envelope's `request_outcomes`.
448    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
449}
450
451/// Typed programmatic audit output before JSON serialization.
452#[derive(Debug, Clone)]
453pub struct AuditProgrammaticOutput {
454    /// Overall audit verdict.
455    pub verdict: AuditVerdict,
456    /// Per-category issue counts.
457    pub summary: AuditSummary,
458    /// New-vs-inherited counts and the configured gate.
459    pub attribution: AuditAttribution,
460    /// Number of changed files the audit analyzed.
461    pub changed_files_count: usize,
462    /// Git revision the audit compared against.
463    pub base_ref: String,
464    /// Human-readable description of how the base was chosen.
465    pub base_description: Option<String>,
466    /// Commit SHA of the analyzed head, when known.
467    pub head_sha: Option<String>,
468    /// Audit wall time, emitted as `elapsed_ms` when serialized.
469    pub elapsed: std::time::Duration,
470    /// Always `None` on the typed route. A skipped base means that the head
471    /// keys stand in for the base, so every finding is inherited. It does not
472    /// mean that no attribution ran.
473    pub base_snapshot_skipped: Option<bool>,
474    /// Attribution key sets of the base run, scoped to the changed files and
475    /// the pre-rename paths, with the rename remap and the dependency scope
476    /// applied. When the head run stands in for the base, it holds the head
477    /// keys. `None` when the gate needs no base or when the changeset is
478    /// empty.
479    pub base_snapshot: Option<AuditProgrammaticKeySnapshot>,
480    /// Dead-code section; `None` when the audit skipped it.
481    pub dead_code: Option<DeadCodeProgrammaticOutput>,
482    /// Duplication section; `None` when the audit skipped it.
483    pub duplication: Option<DuplicationProgrammaticOutput>,
484    /// Complexity (health) section; `None` when the audit skipped it.
485    pub complexity: Option<HealthProgrammaticOutput>,
486    /// Suggested follow-up commands for the consumer.
487    pub next_steps: Vec<NextStep>,
488    /// Analysis run id stamped into telemetry metadata when present.
489    pub telemetry_analysis_run_id: Option<String>,
490}
491
492/// Stable audit key snapshot used to classify introduced vs inherited findings.
493#[derive(Debug, Clone, Default)]
494pub struct AuditProgrammaticKeySnapshot {
495    /// Base-run dead-code attribution keys.
496    pub dead_code: FxHashSet<String>,
497    /// Base-run complexity attribution keys.
498    pub health: FxHashSet<String>,
499    /// Base-run duplication group attribution keys.
500    pub dupes: FxHashSet<String>,
501}
502
503/// Typed programmatic decision-surface output before JSON serialization.
504#[derive(Debug, Clone)]
505pub struct DecisionSurfaceProgrammaticOutput {
506    /// Typed decision surface produced by the run.
507    pub surface: fallow_output::DecisionSurface,
508    /// Analysis wall time, emitted as `elapsed_ms` when serialized.
509    pub elapsed: std::time::Duration,
510    /// Analysis run id stamped into telemetry metadata when present.
511    pub telemetry_analysis_run_id: Option<String>,
512}
513
514/// Serialize a health / complexity report into the stable JSON output contract.
515///
516/// # Errors
517///
518/// Returns a serde error when the report cannot be converted to JSON.
519pub fn serialize_health_report_json(
520    input: HealthJsonReportInput<'_>,
521) -> Result<serde_json::Value, serde_json::Error> {
522    let root_prefix = format!("{}/", input.root.display());
523    let meta = match (input.explain, input.type_aware) {
524        (false, None) => None,
525        (true, None) => Some(health_meta()),
526        (false, Some(type_aware)) => Some(fallow_types::envelope::Meta {
527            type_aware: Some(type_aware),
528            ..fallow_types::envelope::Meta::default()
529        }),
530        (true, Some(type_aware)) => {
531            let mut meta = health_meta();
532            meta.type_aware = Some(type_aware);
533            Some(meta)
534        }
535    };
536    fallow_output::serialize_health_json_output(HealthJsonOutputInput {
537        output: HealthOutputInput {
538            gate_outcomes: input.gate_outcomes,
539            request_outcomes: input.request_outcomes,
540            schema_version: HEALTH_SCHEMA_VERSION,
541            version: env!("CARGO_PKG_VERSION").to_string(),
542            elapsed: input.elapsed,
543            report: input.report,
544            grouped_by: input.grouped_by,
545            groups: input.groups,
546            meta,
547            workspace_diagnostics: input.workspace_diagnostics,
548            next_steps: input.next_steps,
549        },
550        root_prefix: Some(&root_prefix),
551        analysis_run_id: input.telemetry_analysis_run_id,
552    })
553}