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