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