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
5use fallow_output::{
6    CheckOutput, DupesOutput, FeatureFlagFinding, FeatureFlagsOutput as FeatureFlagsOutputContract,
7    GroupByMode, HealthGroup, HealthGrouping, HealthJsonOutputInput, HealthOutputInput,
8    HealthReport, RootEnvelopeMode, health_meta,
9};
10use fallow_types::output::NextStep;
11use fallow_types::output_dead_code::{
12    BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
13    CircularDependencyFinding,
14};
15use fallow_types::results::AnalysisResults;
16use fallow_types::workspace::WorkspaceDiagnostic;
17use rustc_hash::FxHashSet;
18
19use crate::{AuditAttribution, AuditSummary, AuditVerdict};
20use crate::{CloneFamilyFinding, CloneGroupFinding, DupesReportPayload, DuplicationGroup};
21
22pub const HEALTH_SCHEMA_VERSION: u32 = 7;
23
24/// Concrete dead-code output contract returned by typed programmatic runs.
25pub type DeadCodeOutput = CheckOutput;
26
27/// Concrete circular-dependency output contract returned by typed runs.
28pub type CircularDependenciesOutput = CheckOutput;
29
30/// Concrete boundary-family output contract returned by typed runs.
31pub type BoundaryViolationsOutput = CheckOutput;
32
33/// Concrete duplication output contract returned by typed programmatic runs.
34pub type DuplicationOutput = DupesOutput<DupesReportPayload, DuplicationGroup>;
35
36/// Concrete feature-flag output contract returned by typed programmatic runs.
37pub type FeatureFlagsOutput = FeatureFlagsOutputContract;
38
39/// Concrete export trace output returned by typed programmatic runs.
40pub type TraceExportOutput = fallow_types::trace::ExportTrace;
41
42/// Concrete class / enum / store member trace output (the `trace_export`
43/// fallback when the name is a member rather than a top-level export). See
44/// issue #1744.
45pub type TraceClassMemberOutput = fallow_types::trace::ClassMemberTrace;
46
47/// The `trace_export` target: either a top-level export or (fallback) a class /
48/// enum / store member declared on one. Serialized untagged so the export shape
49/// stays byte-identical to the historical contract and the member shape matches
50/// the CLI's member trace; consumers distinguish by the presence of
51/// `export_name` (export) vs `member_name` / `owner_export` (member).
52#[derive(Debug, serde::Serialize)]
53#[serde(untagged)]
54pub enum TraceExportTargetOutput {
55    /// A top-level export trace.
56    Export(TraceExportOutput),
57    /// A class / enum / store member trace.
58    Member(TraceClassMemberOutput),
59}
60
61/// Concrete file trace output returned by typed programmatic runs.
62pub type TraceFileOutput = fallow_types::trace::FileTrace;
63
64/// Concrete dependency trace output returned by typed programmatic runs.
65pub type TraceDependencyOutput = fallow_types::trace::DependencyTrace;
66
67/// Concrete duplicate-code trace output returned by typed programmatic runs.
68pub type TraceCloneOutput = fallow_types::trace::CloneTrace;
69
70/// Inputs for serializing health JSON output through the API boundary.
71pub struct HealthJsonReportInput<'a> {
72    pub report: HealthReport,
73    pub root: &'a Path,
74    pub elapsed: std::time::Duration,
75    pub explain: bool,
76    pub type_aware: Option<fallow_types::envelope::TypeAwareMeta>,
77    pub grouped_by: Option<GroupByMode>,
78    pub groups: Option<Vec<HealthGroup>>,
79    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
80    pub next_steps: Vec<NextStep>,
81    pub envelope_mode: RootEnvelopeMode,
82    pub telemetry_analysis_run_id: Option<&'a str>,
83}
84
85/// Typed programmatic combined output before JSON serialization.
86#[derive(Debug, Clone)]
87pub struct CombinedProgrammaticOutput {
88    pub dead_code: Option<DeadCodeProgrammaticOutput>,
89    pub duplication: Option<DuplicationProgrammaticOutput>,
90    pub health: Option<HealthProgrammaticOutput>,
91    pub root: PathBuf,
92    pub elapsed: std::time::Duration,
93    pub explain: bool,
94    pub next_steps: Vec<NextStep>,
95    pub envelope_mode: RootEnvelopeMode,
96    pub telemetry_analysis_run_id: Option<String>,
97}
98
99/// Typed programmatic dead-code output before JSON serialization.
100///
101/// This is the API boundary embedders should use when they need access to the
102/// typed engine/output result. Protocol surfaces serialize it explicitly at
103/// their JSON boundary.
104#[derive(Debug, Clone)]
105pub struct DeadCodeProgrammaticOutput {
106    pub output: DeadCodeOutput,
107    pub root: PathBuf,
108    pub config_fixable: bool,
109    pub envelope_mode: RootEnvelopeMode,
110    pub telemetry_analysis_run_id: Option<String>,
111}
112
113impl DeadCodeProgrammaticOutput {
114    /// Full typed dead-code issue arrays retained by this run.
115    #[must_use]
116    pub fn results(&self) -> &AnalysisResults {
117        &self.output.results
118    }
119
120    /// Project-relative root used when serializing stable JSON paths.
121    #[must_use]
122    pub fn root(&self) -> &Path {
123        &self.root
124    }
125}
126
127/// Typed programmatic circular-dependency output before JSON serialization.
128///
129/// The wire envelope stays the dead-code/check contract, but the Rust API
130/// surface is family-specific so embedders do not have to treat this as a
131/// generic dead-code run.
132#[derive(Debug, Clone)]
133pub struct CircularDependenciesProgrammaticOutput {
134    pub output: CircularDependenciesOutput,
135    pub root: PathBuf,
136    pub envelope_mode: RootEnvelopeMode,
137    pub telemetry_analysis_run_id: Option<String>,
138}
139
140impl CircularDependenciesProgrammaticOutput {
141    /// Full typed issue arrays retained by this family run.
142    #[must_use]
143    pub fn results(&self) -> &AnalysisResults {
144        &self.output.results
145    }
146
147    /// The circular dependency findings retained by this family run.
148    #[must_use]
149    pub fn circular_dependencies(&self) -> &[CircularDependencyFinding] {
150        &self.output.results.circular_dependencies
151    }
152}
153
154impl From<DeadCodeProgrammaticOutput> for CircularDependenciesProgrammaticOutput {
155    fn from(value: DeadCodeProgrammaticOutput) -> Self {
156        Self {
157            output: value.output,
158            root: value.root,
159            envelope_mode: value.envelope_mode,
160            telemetry_analysis_run_id: value.telemetry_analysis_run_id,
161        }
162    }
163}
164
165/// Typed programmatic boundary-family output before JSON serialization.
166///
167/// This covers banned imports, boundary coverage, and forbidden call findings
168/// while preserving the stable dead-code/check JSON envelope.
169#[derive(Debug, Clone)]
170pub struct BoundaryViolationsProgrammaticOutput {
171    pub output: BoundaryViolationsOutput,
172    pub root: PathBuf,
173    pub envelope_mode: RootEnvelopeMode,
174    pub telemetry_analysis_run_id: Option<String>,
175}
176
177impl BoundaryViolationsProgrammaticOutput {
178    /// Full typed issue arrays retained by this family run.
179    #[must_use]
180    pub fn results(&self) -> &AnalysisResults {
181        &self.output.results
182    }
183
184    /// Banned import boundary findings retained by this family run.
185    #[must_use]
186    pub fn boundary_violations(&self) -> &[BoundaryViolationFinding] {
187        &self.output.results.boundary_violations
188    }
189
190    /// Boundary coverage findings retained by this family run.
191    #[must_use]
192    pub fn boundary_coverage_violations(&self) -> &[BoundaryCoverageViolationFinding] {
193        &self.output.results.boundary_coverage_violations
194    }
195
196    /// Forbidden call findings retained by this family run.
197    #[must_use]
198    pub fn boundary_call_violations(&self) -> &[BoundaryCallViolationFinding] {
199        &self.output.results.boundary_call_violations
200    }
201}
202
203impl From<DeadCodeProgrammaticOutput> for BoundaryViolationsProgrammaticOutput {
204    fn from(value: DeadCodeProgrammaticOutput) -> Self {
205        Self {
206            output: value.output,
207            root: value.root,
208            envelope_mode: value.envelope_mode,
209            telemetry_analysis_run_id: value.telemetry_analysis_run_id,
210        }
211    }
212}
213
214/// Typed programmatic duplication output before JSON serialization.
215#[derive(Debug, Clone)]
216pub struct DuplicationProgrammaticOutput {
217    pub output: DuplicationOutput,
218    pub root: PathBuf,
219    pub threshold: f64,
220    pub envelope_mode: RootEnvelopeMode,
221    pub telemetry_analysis_run_id: Option<String>,
222}
223
224impl DuplicationProgrammaticOutput {
225    /// Typed duplication report payload retained by this run.
226    #[must_use]
227    pub const fn report(&self) -> &DupesReportPayload {
228        &self.output.report
229    }
230
231    /// Clone groups retained by this run, with typed actions and fingerprints.
232    #[must_use]
233    pub fn clone_groups(&self) -> &[CloneGroupFinding] {
234        &self.output.report.clone_groups
235    }
236
237    /// Clone families retained by this run, with nested typed clone groups.
238    #[must_use]
239    pub fn clone_families(&self) -> &[CloneFamilyFinding] {
240        &self.output.report.clone_families
241    }
242
243    /// Grouped duplication buckets when a grouping mode was used.
244    #[must_use]
245    pub fn groups(&self) -> Option<&[DuplicationGroup]> {
246        self.output.groups.as_deref()
247    }
248}
249
250/// Typed programmatic feature-flag output before JSON serialization.
251#[derive(Debug, Clone)]
252pub struct FeatureFlagsProgrammaticOutput {
253    pub output: FeatureFlagsOutput,
254    pub envelope_mode: RootEnvelopeMode,
255    pub telemetry_analysis_run_id: Option<String>,
256}
257
258impl FeatureFlagsProgrammaticOutput {
259    /// Feature flag findings retained by this run.
260    #[must_use]
261    pub fn feature_flags(&self) -> &[FeatureFlagFinding] {
262        &self.output.feature_flags
263    }
264
265    /// Number of feature flags retained by this run after scoping and limits.
266    #[must_use]
267    pub const fn total_flags(&self) -> usize {
268        self.output.total_flags
269    }
270}
271
272/// Typed programmatic export-trace output before JSON serialization. Carries
273/// either an export trace or (fallback) a class / enum / store member trace.
274#[derive(Debug)]
275pub struct TraceExportProgrammaticOutput {
276    pub output: TraceExportTargetOutput,
277}
278
279impl TraceExportProgrammaticOutput {
280    /// Typed export-or-member trace retained by this run.
281    #[must_use]
282    pub const fn trace(&self) -> &TraceExportTargetOutput {
283        &self.output
284    }
285
286    /// The export trace, when the target resolved to a top-level export.
287    #[must_use]
288    pub const fn as_export(&self) -> Option<&TraceExportOutput> {
289        match &self.output {
290            TraceExportTargetOutput::Export(export) => Some(export),
291            TraceExportTargetOutput::Member(_) => None,
292        }
293    }
294
295    /// The member trace, when the target resolved to a class / enum / store
296    /// member (the `trace_export` fallback, issue #1744).
297    #[must_use]
298    pub const fn as_member(&self) -> Option<&TraceClassMemberOutput> {
299        match &self.output {
300            TraceExportTargetOutput::Member(member) => Some(member),
301            TraceExportTargetOutput::Export(_) => None,
302        }
303    }
304}
305
306/// Typed programmatic file-trace output before JSON serialization.
307#[derive(Debug)]
308pub struct TraceFileProgrammaticOutput {
309    pub output: TraceFileOutput,
310}
311
312impl TraceFileProgrammaticOutput {
313    /// Typed file trace retained by this run.
314    #[must_use]
315    pub const fn trace(&self) -> &TraceFileOutput {
316        &self.output
317    }
318}
319
320/// Typed programmatic dependency-trace output before JSON serialization.
321#[derive(Debug)]
322pub struct TraceDependencyProgrammaticOutput {
323    pub output: TraceDependencyOutput,
324}
325
326impl TraceDependencyProgrammaticOutput {
327    /// Typed dependency trace retained by this run.
328    #[must_use]
329    pub const fn trace(&self) -> &TraceDependencyOutput {
330        &self.output
331    }
332}
333
334/// Typed programmatic duplicate-code trace output before JSON serialization.
335#[derive(Debug)]
336pub struct TraceCloneProgrammaticOutput {
337    pub output: TraceCloneOutput,
338}
339
340impl TraceCloneProgrammaticOutput {
341    /// Typed clone trace retained by this run.
342    #[must_use]
343    pub const fn trace(&self) -> &TraceCloneOutput {
344        &self.output
345    }
346}
347
348/// Typed programmatic health / complexity output before JSON serialization.
349#[derive(Debug, Clone)]
350pub struct HealthProgrammaticOutput {
351    pub report: HealthReport,
352    pub grouping: Option<HealthGrouping>,
353    pub root: PathBuf,
354    pub elapsed: std::time::Duration,
355    pub explain: bool,
356    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
357    pub next_steps: Vec<NextStep>,
358    pub envelope_mode: RootEnvelopeMode,
359    pub telemetry_analysis_run_id: Option<String>,
360}
361
362/// Typed programmatic audit output before JSON serialization.
363#[derive(Debug, Clone)]
364pub struct AuditProgrammaticOutput {
365    pub verdict: AuditVerdict,
366    pub summary: AuditSummary,
367    pub attribution: AuditAttribution,
368    pub changed_files_count: usize,
369    pub base_ref: String,
370    pub base_description: Option<String>,
371    pub head_sha: Option<String>,
372    pub elapsed: std::time::Duration,
373    pub base_snapshot_skipped: Option<bool>,
374    pub base_snapshot: Option<AuditProgrammaticKeySnapshot>,
375    pub dead_code: Option<DeadCodeProgrammaticOutput>,
376    pub duplication: Option<DuplicationProgrammaticOutput>,
377    pub complexity: Option<HealthProgrammaticOutput>,
378    pub next_steps: Vec<NextStep>,
379    pub envelope_mode: RootEnvelopeMode,
380    pub telemetry_analysis_run_id: Option<String>,
381}
382
383/// Stable audit key snapshot used to classify introduced vs inherited findings.
384#[derive(Debug, Clone, Default)]
385pub struct AuditProgrammaticKeySnapshot {
386    pub dead_code: FxHashSet<String>,
387    pub health: FxHashSet<String>,
388    pub dupes: FxHashSet<String>,
389}
390
391/// Typed programmatic decision-surface output before JSON serialization.
392#[derive(Debug, Clone)]
393pub struct DecisionSurfaceProgrammaticOutput {
394    pub surface: fallow_output::DecisionSurface,
395    pub elapsed: std::time::Duration,
396    pub envelope_mode: RootEnvelopeMode,
397    pub telemetry_analysis_run_id: Option<String>,
398}
399
400/// Serialize a health / complexity report into the stable JSON output contract.
401///
402/// # Errors
403///
404/// Returns a serde error when the report cannot be converted to JSON.
405pub fn serialize_health_report_json(
406    input: HealthJsonReportInput<'_>,
407) -> Result<serde_json::Value, serde_json::Error> {
408    let root_prefix = format!("{}/", input.root.display());
409    let meta = match (input.explain, input.type_aware) {
410        (false, None) => None,
411        (true, None) => Some(health_meta()),
412        (false, Some(type_aware)) => Some(fallow_types::envelope::Meta {
413            type_aware: Some(type_aware),
414            ..fallow_types::envelope::Meta::default()
415        }),
416        (true, Some(type_aware)) => {
417            let mut meta = health_meta();
418            meta.type_aware = Some(type_aware);
419            Some(meta)
420        }
421    };
422    fallow_output::serialize_health_json_output(HealthJsonOutputInput {
423        output: HealthOutputInput {
424            schema_version: HEALTH_SCHEMA_VERSION,
425            version: env!("CARGO_PKG_VERSION").to_string(),
426            elapsed: input.elapsed,
427            report: input.report,
428            grouped_by: input.grouped_by,
429            groups: input.groups,
430            meta,
431            workspace_diagnostics: input.workspace_diagnostics,
432            next_steps: input.next_steps,
433        },
434        root_prefix: Some(&root_prefix),
435        envelope_mode: input.envelope_mode,
436        analysis_run_id: input.telemetry_analysis_run_id,
437    })
438}