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, RootEnvelopeMode, 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    /// Whether the root envelope carries a `kind` discriminant.
105    pub envelope_mode: RootEnvelopeMode,
106    /// Analysis run id stamped into telemetry metadata when present.
107    pub telemetry_analysis_run_id: Option<&'a str>,
108}
109
110/// Typed programmatic combined output before JSON serialization.
111#[derive(Debug, Clone)]
112pub struct CombinedProgrammaticOutput {
113    /// Dead-code section; `None` when the run skipped it.
114    pub dead_code: Option<DeadCodeProgrammaticOutput>,
115    /// Duplication section; `None` when the run skipped it.
116    pub duplication: Option<DuplicationProgrammaticOutput>,
117    /// Health section; `None` when the run skipped it.
118    pub health: Option<HealthProgrammaticOutput>,
119    /// Project root used when serializing stable JSON paths.
120    pub root: PathBuf,
121    /// Total wall time across sections, emitted as the root `elapsed_ms`.
122    pub elapsed: std::time::Duration,
123    /// Emit per-section explain metadata when serialized.
124    pub explain: bool,
125    /// Suggested follow-up commands for the consumer.
126    pub next_steps: Vec<NextStep>,
127    /// Whether the serialized root envelope carries a `kind` discriminant.
128    pub envelope_mode: RootEnvelopeMode,
129    /// Analysis run id stamped into telemetry metadata when present.
130    pub telemetry_analysis_run_id: Option<String>,
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    /// Whether the serialized root envelope carries a `kind` discriminant.
148    pub envelope_mode: RootEnvelopeMode,
149    /// Analysis run id stamped into telemetry metadata when present.
150    pub telemetry_analysis_run_id: Option<String>,
151}
152
153impl DeadCodeProgrammaticOutput {
154    /// Full typed dead-code issue arrays retained by this run.
155    #[must_use]
156    pub fn results(&self) -> &AnalysisResults {
157        &self.output.results
158    }
159
160    /// Project-relative root used when serializing stable JSON paths.
161    #[must_use]
162    pub fn root(&self) -> &Path {
163        &self.root
164    }
165}
166
167/// Typed programmatic circular-dependency output before JSON serialization.
168///
169/// The wire envelope stays the dead-code/check contract, but the Rust API
170/// surface is family-specific so embedders do not have to treat this as a
171/// generic dead-code run.
172#[derive(Debug, Clone)]
173pub struct CircularDependenciesProgrammaticOutput {
174    /// Typed check envelope scoped to circular-dependency findings.
175    pub output: CircularDependenciesOutput,
176    /// Project root used when serializing stable JSON paths.
177    pub root: PathBuf,
178    /// Whether the serialized root envelope carries a `kind` discriminant.
179    pub envelope_mode: RootEnvelopeMode,
180    /// Analysis run id stamped into telemetry metadata when present.
181    pub telemetry_analysis_run_id: Option<String>,
182}
183
184impl CircularDependenciesProgrammaticOutput {
185    /// Full typed issue arrays retained by this family run.
186    #[must_use]
187    pub fn results(&self) -> &AnalysisResults {
188        &self.output.results
189    }
190
191    /// The circular dependency findings retained by this family run.
192    #[must_use]
193    pub fn circular_dependencies(&self) -> &[CircularDependencyFinding] {
194        &self.output.results.circular_dependencies
195    }
196}
197
198impl From<DeadCodeProgrammaticOutput> for CircularDependenciesProgrammaticOutput {
199    fn from(value: DeadCodeProgrammaticOutput) -> Self {
200        Self {
201            output: value.output,
202            root: value.root,
203            envelope_mode: value.envelope_mode,
204            telemetry_analysis_run_id: value.telemetry_analysis_run_id,
205        }
206    }
207}
208
209/// Typed programmatic boundary-family output before JSON serialization.
210///
211/// This covers banned imports, boundary coverage, and forbidden call findings
212/// while preserving the stable dead-code/check JSON envelope.
213#[derive(Debug, Clone)]
214pub struct BoundaryViolationsProgrammaticOutput {
215    /// Typed check envelope scoped to boundary-family findings.
216    pub output: BoundaryViolationsOutput,
217    /// Project root used when serializing stable JSON paths.
218    pub root: PathBuf,
219    /// Whether the serialized root envelope carries a `kind` discriminant.
220    pub envelope_mode: RootEnvelopeMode,
221    /// Analysis run id stamped into telemetry metadata when present.
222    pub telemetry_analysis_run_id: Option<String>,
223}
224
225impl BoundaryViolationsProgrammaticOutput {
226    /// Full typed issue arrays retained by this family run.
227    #[must_use]
228    pub fn results(&self) -> &AnalysisResults {
229        &self.output.results
230    }
231
232    /// Banned import boundary findings retained by this family run.
233    #[must_use]
234    pub fn boundary_violations(&self) -> &[BoundaryViolationFinding] {
235        &self.output.results.boundary_violations
236    }
237
238    /// Boundary coverage findings retained by this family run.
239    #[must_use]
240    pub fn boundary_coverage_violations(&self) -> &[BoundaryCoverageViolationFinding] {
241        &self.output.results.boundary_coverage_violations
242    }
243
244    /// Forbidden call findings retained by this family run.
245    #[must_use]
246    pub fn boundary_call_violations(&self) -> &[BoundaryCallViolationFinding] {
247        &self.output.results.boundary_call_violations
248    }
249}
250
251impl From<DeadCodeProgrammaticOutput> for BoundaryViolationsProgrammaticOutput {
252    fn from(value: DeadCodeProgrammaticOutput) -> Self {
253        Self {
254            output: value.output,
255            root: value.root,
256            envelope_mode: value.envelope_mode,
257            telemetry_analysis_run_id: value.telemetry_analysis_run_id,
258        }
259    }
260}
261
262/// Typed programmatic duplication output before JSON serialization.
263#[derive(Debug, Clone)]
264pub struct DuplicationProgrammaticOutput {
265    /// Typed duplication envelope produced by the run.
266    pub output: DuplicationOutput,
267    /// Project root used when serializing stable JSON paths.
268    pub root: PathBuf,
269    /// Maximum allowed duplication percentage from the resolved config;
270    /// 0 disables the percentage gate.
271    pub threshold: f64,
272    /// Whether the serialized root envelope carries a `kind` discriminant.
273    pub envelope_mode: RootEnvelopeMode,
274    /// Analysis run id stamped into telemetry metadata when present.
275    pub telemetry_analysis_run_id: Option<String>,
276}
277
278impl DuplicationProgrammaticOutput {
279    /// Typed duplication report payload retained by this run.
280    #[must_use]
281    pub const fn report(&self) -> &DupesReportPayload {
282        &self.output.report
283    }
284
285    /// Clone groups retained by this run, with typed actions and fingerprints.
286    #[must_use]
287    pub fn clone_groups(&self) -> &[CloneGroupFinding] {
288        &self.output.report.clone_groups
289    }
290
291    /// Clone families retained by this run, with nested typed clone groups.
292    #[must_use]
293    pub fn clone_families(&self) -> &[CloneFamilyFinding] {
294        &self.output.report.clone_families
295    }
296
297    /// Grouped duplication buckets when a grouping mode was used.
298    #[must_use]
299    pub fn groups(&self) -> Option<&[DuplicationGroup]> {
300        self.output.groups.as_deref()
301    }
302}
303
304/// Typed programmatic feature-flag output before JSON serialization.
305#[derive(Debug, Clone)]
306pub struct FeatureFlagsProgrammaticOutput {
307    /// Typed feature-flag envelope produced by the run.
308    pub output: FeatureFlagsOutput,
309    /// Whether the serialized root envelope carries a `kind` discriminant.
310    pub envelope_mode: RootEnvelopeMode,
311    /// Analysis run id stamped into telemetry metadata when present.
312    pub telemetry_analysis_run_id: Option<String>,
313}
314
315impl FeatureFlagsProgrammaticOutput {
316    /// Feature flag findings retained by this run.
317    #[must_use]
318    pub fn feature_flags(&self) -> &[FeatureFlagFinding] {
319        &self.output.feature_flags
320    }
321
322    /// Number of feature flags retained by this run after scoping and limits.
323    #[must_use]
324    pub const fn total_flags(&self) -> usize {
325        self.output.total_flags
326    }
327}
328
329/// Typed programmatic export-trace output before JSON serialization. Carries
330/// either an export trace or (fallback) a class / enum / store member trace.
331#[derive(Debug)]
332pub struct TraceExportProgrammaticOutput {
333    /// Typed export-or-member trace produced by the run.
334    pub output: TraceExportTargetOutput,
335}
336
337impl TraceExportProgrammaticOutput {
338    /// Typed export-or-member trace retained by this run.
339    #[must_use]
340    pub const fn trace(&self) -> &TraceExportTargetOutput {
341        &self.output
342    }
343
344    /// The export trace, when the target resolved to a top-level export.
345    #[must_use]
346    pub const fn as_export(&self) -> Option<&TraceExportOutput> {
347        match &self.output {
348            TraceExportTargetOutput::Export(export) => Some(export),
349            TraceExportTargetOutput::Member(_) => None,
350        }
351    }
352
353    /// The member trace, when the target resolved to a class / enum / store
354    /// member (the `trace_export` fallback, issue #1744).
355    #[must_use]
356    pub const fn as_member(&self) -> Option<&TraceClassMemberOutput> {
357        match &self.output {
358            TraceExportTargetOutput::Member(member) => Some(member),
359            TraceExportTargetOutput::Export(_) => None,
360        }
361    }
362}
363
364/// Typed programmatic file-trace output before JSON serialization.
365#[derive(Debug)]
366pub struct TraceFileProgrammaticOutput {
367    /// Typed file trace produced by the run.
368    pub output: TraceFileOutput,
369}
370
371impl TraceFileProgrammaticOutput {
372    /// Typed file trace retained by this run.
373    #[must_use]
374    pub const fn trace(&self) -> &TraceFileOutput {
375        &self.output
376    }
377}
378
379/// Typed programmatic import-path-trace output before JSON serialization.
380#[derive(Debug)]
381pub struct TraceImportPathProgrammaticOutput {
382    /// Typed import-path trace produced by the run.
383    pub output: TraceImportPathOutput,
384}
385
386impl TraceImportPathProgrammaticOutput {
387    /// Typed import-path trace retained by this run.
388    #[must_use]
389    pub const fn trace(&self) -> &TraceImportPathOutput {
390        &self.output
391    }
392}
393
394/// Typed programmatic dependency-trace output before JSON serialization.
395#[derive(Debug)]
396pub struct TraceDependencyProgrammaticOutput {
397    /// Typed dependency trace produced by the run.
398    pub output: TraceDependencyOutput,
399}
400
401impl TraceDependencyProgrammaticOutput {
402    /// Typed dependency trace retained by this run.
403    #[must_use]
404    pub const fn trace(&self) -> &TraceDependencyOutput {
405        &self.output
406    }
407}
408
409/// Typed programmatic stack-trace resolution before JSON serialization.
410#[derive(Debug)]
411pub struct TraceErrorProgrammaticOutput {
412    /// Typed stack-trace resolution produced by the run.
413    pub output: TraceErrorOutput,
414}
415
416impl TraceErrorProgrammaticOutput {
417    /// Typed stack-trace resolution retained by this run.
418    #[must_use]
419    pub const fn trace(&self) -> &TraceErrorOutput {
420        &self.output
421    }
422}
423
424/// Typed programmatic duplicate-code trace output before JSON serialization.
425#[derive(Debug)]
426pub struct TraceCloneProgrammaticOutput {
427    /// Typed clone trace produced by the run.
428    pub output: TraceCloneOutput,
429}
430
431impl TraceCloneProgrammaticOutput {
432    /// Typed clone trace retained by this run.
433    #[must_use]
434    pub const fn trace(&self) -> &TraceCloneOutput {
435        &self.output
436    }
437}
438
439/// Typed programmatic health / complexity output before JSON serialization.
440#[derive(Debug, Clone)]
441pub struct HealthProgrammaticOutput {
442    /// Typed health report produced by the run.
443    pub report: HealthReport,
444    /// Grouped findings when a grouping mode was requested.
445    pub grouping: Option<HealthGrouping>,
446    /// Project root used when serializing stable JSON paths.
447    pub root: PathBuf,
448    /// Analysis wall time, emitted as `elapsed_ms` when serialized.
449    pub elapsed: std::time::Duration,
450    /// Emit explain metadata when serialized.
451    pub explain: bool,
452    /// Non-fatal per-file diagnostics collected during the workspace walk.
453    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
454    /// Suggested follow-up commands for the consumer.
455    pub next_steps: Vec<NextStep>,
456    /// Whether the serialized root envelope carries a `kind` discriminant.
457    pub envelope_mode: RootEnvelopeMode,
458    /// Analysis run id stamped into telemetry metadata when present.
459    pub telemetry_analysis_run_id: Option<String>,
460}
461
462/// Typed programmatic audit output before JSON serialization.
463#[derive(Debug, Clone)]
464pub struct AuditProgrammaticOutput {
465    /// Overall audit verdict.
466    pub verdict: AuditVerdict,
467    /// Per-category issue counts.
468    pub summary: AuditSummary,
469    /// New-vs-inherited counts and the configured gate.
470    pub attribution: AuditAttribution,
471    /// Number of changed files the audit analyzed.
472    pub changed_files_count: usize,
473    /// Git revision the audit compared against.
474    pub base_ref: String,
475    /// Human-readable description of how the base was chosen.
476    pub base_description: Option<String>,
477    /// Commit SHA of the analyzed head, when known.
478    pub head_sha: Option<String>,
479    /// Audit wall time, emitted as `elapsed_ms` when serialized.
480    pub elapsed: std::time::Duration,
481    /// `Some(true)` when the base snapshot was skipped, so no attribution
482    /// ran.
483    pub base_snapshot_skipped: Option<bool>,
484    /// Attribution key sets computed from the base run; present only when a
485    /// base snapshot was analyzed.
486    pub base_snapshot: Option<AuditProgrammaticKeySnapshot>,
487    /// Dead-code section; `None` when the audit skipped it.
488    pub dead_code: Option<DeadCodeProgrammaticOutput>,
489    /// Duplication section; `None` when the audit skipped it.
490    pub duplication: Option<DuplicationProgrammaticOutput>,
491    /// Complexity (health) section; `None` when the audit skipped it.
492    pub complexity: Option<HealthProgrammaticOutput>,
493    /// Suggested follow-up commands for the consumer.
494    pub next_steps: Vec<NextStep>,
495    /// Whether the serialized root envelope carries a `kind` discriminant.
496    pub envelope_mode: RootEnvelopeMode,
497    /// Analysis run id stamped into telemetry metadata when present.
498    pub telemetry_analysis_run_id: Option<String>,
499}
500
501/// Stable audit key snapshot used to classify introduced vs inherited findings.
502#[derive(Debug, Clone, Default)]
503pub struct AuditProgrammaticKeySnapshot {
504    /// Base-run dead-code attribution keys.
505    pub dead_code: FxHashSet<String>,
506    /// Base-run complexity attribution keys.
507    pub health: FxHashSet<String>,
508    /// Base-run duplication group attribution keys.
509    pub dupes: FxHashSet<String>,
510}
511
512/// Typed programmatic decision-surface output before JSON serialization.
513#[derive(Debug, Clone)]
514pub struct DecisionSurfaceProgrammaticOutput {
515    /// Typed decision surface produced by the run.
516    pub surface: fallow_output::DecisionSurface,
517    /// Analysis wall time, emitted as `elapsed_ms` when serialized.
518    pub elapsed: std::time::Duration,
519    /// Whether the serialized root envelope carries a `kind` discriminant.
520    pub envelope_mode: RootEnvelopeMode,
521    /// Analysis run id stamped into telemetry metadata when present.
522    pub telemetry_analysis_run_id: Option<String>,
523}
524
525/// Serialize a health / complexity report into the stable JSON output contract.
526///
527/// # Errors
528///
529/// Returns a serde error when the report cannot be converted to JSON.
530pub fn serialize_health_report_json(
531    input: HealthJsonReportInput<'_>,
532) -> Result<serde_json::Value, serde_json::Error> {
533    let root_prefix = format!("{}/", input.root.display());
534    let meta = match (input.explain, input.type_aware) {
535        (false, None) => None,
536        (true, None) => Some(health_meta()),
537        (false, Some(type_aware)) => Some(fallow_types::envelope::Meta {
538            type_aware: Some(type_aware),
539            ..fallow_types::envelope::Meta::default()
540        }),
541        (true, Some(type_aware)) => {
542            let mut meta = health_meta();
543            meta.type_aware = Some(type_aware);
544            Some(meta)
545        }
546    };
547    fallow_output::serialize_health_json_output(HealthJsonOutputInput {
548        output: HealthOutputInput {
549            gate_outcomes: input.gate_outcomes,
550            request_outcomes: input.request_outcomes,
551            schema_version: HEALTH_SCHEMA_VERSION,
552            version: env!("CARGO_PKG_VERSION").to_string(),
553            elapsed: input.elapsed,
554            report: input.report,
555            grouped_by: input.grouped_by,
556            groups: input.groups,
557            meta,
558            workspace_diagnostics: input.workspace_diagnostics,
559            next_steps: input.next_steps,
560        },
561        root_prefix: Some(&root_prefix),
562        envelope_mode: input.envelope_mode,
563        analysis_run_id: input.telemetry_analysis_run_id,
564    })
565}