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