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