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