Skip to main content

fallow_api/
runtime_json.rs

1//! JSON protocol serializers for typed programmatic runtime output.
2//!
3//! Runtime entry points return typed output from [`crate::runtime`]. CLI, MCP,
4//! NAPI, and other protocol surfaces call these serializers at their JSON
5//! boundary.
6
7use crate::{
8    ProgrammaticError,
9    runtime::{
10        AuditProgrammaticOutput, BoundaryViolationsProgrammaticOutput,
11        CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput,
12        DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput,
13        DuplicationProgrammaticOutput, FeatureFlagsProgrammaticOutput, HealthJsonReportInput,
14        HealthProgrammaticOutput, TraceCloneProgrammaticOutput, TraceDependencyProgrammaticOutput,
15        TraceExportProgrammaticOutput, TraceFileProgrammaticOutput, serialize_health_report_json,
16    },
17};
18use fallow_output::{
19    CHECK_SCHEMA_VERSION, CheckOutput, GroupByMode, RootEnvelopeMode,
20    build_decision_surface_output, serialize_check_json_output,
21    serialize_decision_surface_json_output, serialize_dupes_json_output,
22    serialize_feature_flags_json_output, strip_root_prefix,
23};
24use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
25use serde::Serialize;
26use std::path::Path;
27use std::time::Duration;
28
29type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
30
31/// Serialize typed combined output into the stable JSON compatibility contract.
32///
33/// # Errors
34///
35/// Returns a structured error if one of the combined sections cannot serialize.
36pub fn serialize_combined_programmatic_json(
37    output: CombinedProgrammaticOutput,
38) -> ProgrammaticResult<serde_json::Value> {
39    let CombinedProgrammaticOutput {
40        dead_code,
41        duplication,
42        health,
43        root,
44        elapsed,
45        explain,
46        next_steps,
47        envelope_mode,
48        telemetry_analysis_run_id,
49    } = output;
50    crate::serialize_combined_json(crate::CombinedJsonOutputInput {
51        check: dead_code
52            .as_ref()
53            .map(|dead_code| crate::CombinedCheckJsonSection {
54                results: &dead_code.output.results,
55                root: &dead_code.root,
56                elapsed: Duration::from_millis(dead_code.output.elapsed_ms.0),
57                config_fixable: dead_code.config_fixable,
58                extras: crate::CheckJsonExtraOutputs::default(),
59            }),
60        dupes: duplication
61            .as_ref()
62            .map(|duplication| &duplication.output.report),
63        health: health.as_ref().map(|health| &health.report),
64        root: &root,
65        elapsed,
66        explain,
67        next_steps,
68        envelope_mode,
69        telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
70    })
71    .map_err(|err| {
72        ProgrammaticError::new(format!("failed to serialize combined report: {err}"), 2)
73            .with_code("FALLOW_SERIALIZE_COMBINED_REPORT")
74            .with_context("combined")
75    })
76}
77
78/// Serialize typed decision-surface output into the stable JSON contract.
79///
80/// # Errors
81///
82/// Returns a structured error if the decision-surface payload cannot serialize.
83pub fn serialize_decision_surface_programmatic_json(
84    output: DecisionSurfaceProgrammaticOutput,
85) -> ProgrammaticResult<serde_json::Value> {
86    let DecisionSurfaceProgrammaticOutput {
87        surface,
88        elapsed: _,
89        envelope_mode,
90        telemetry_analysis_run_id,
91    } = output;
92    let payload = build_decision_surface_output(&surface);
93    serialize_decision_surface_json_output(
94        payload,
95        envelope_mode,
96        telemetry_analysis_run_id.as_deref(),
97    )
98    .map_err(|err| {
99        ProgrammaticError::new(format!("failed to serialize decision surface: {err}"), 2)
100            .with_code("FALLOW_SERIALIZE_DECISION_SURFACE")
101            .with_context("decision-surface")
102    })
103}
104
105/// Serialize typed audit output into the stable JSON compatibility contract.
106///
107/// # Errors
108///
109/// Returns a structured error if one of the audit sections cannot serialize.
110pub fn serialize_audit_programmatic_json(
111    output: AuditProgrammaticOutput,
112) -> ProgrammaticResult<serde_json::Value> {
113    let base_snapshot = output.base_snapshot.as_ref();
114    let dead_code = output
115        .dead_code
116        .as_ref()
117        .map(|dead_code| serialize_audit_dead_code(dead_code, base_snapshot))
118        .transpose()?;
119    let duplication = output
120        .duplication
121        .as_ref()
122        .map(|duplication| serialize_audit_duplication(duplication, base_snapshot))
123        .transpose()?;
124    let complexity = output
125        .complexity
126        .as_ref()
127        .map(|complexity| serialize_audit_complexity(complexity, base_snapshot))
128        .transpose()?;
129
130    crate::serialize_audit_json(
131        crate::AuditJsonOutputInput {
132            header: crate::AuditJsonHeaderInput {
133                schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
134                version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
135                verdict: output.verdict,
136                changed_files_count: u32::try_from(output.changed_files_count).unwrap_or(u32::MAX),
137                base_ref: output.base_ref,
138                base_description: output.base_description,
139                head_sha: output.head_sha,
140                elapsed_ms: ElapsedMs(
141                    u64::try_from(output.elapsed.as_millis()).unwrap_or(u64::MAX),
142                ),
143                base_snapshot_skipped: output.base_snapshot_skipped,
144                summary: output.summary,
145                attribution: output.attribution,
146            },
147            dead_code,
148            duplication,
149            complexity,
150            next_steps: output.next_steps,
151        },
152        output.envelope_mode,
153        output.telemetry_analysis_run_id.as_deref(),
154    )
155    .map_err(|err| {
156        ProgrammaticError::new(format!("failed to serialize audit report: {err}"), 2)
157            .with_code("FALLOW_SERIALIZE_AUDIT_REPORT")
158            .with_context("audit")
159    })
160}
161
162fn serialize_audit_dead_code(
163    output: &DeadCodeProgrammaticOutput,
164    base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
165) -> ProgrammaticResult<serde_json::Value> {
166    let mut json = crate::serialize_check_json_payload(crate::CheckJsonPayloadInput {
167        results: &output.output.results,
168        root: &output.root,
169        elapsed: Duration::from_millis(output.output.elapsed_ms.0),
170        config_fixable: output.config_fixable,
171        extras: crate::CheckJsonExtraOutputs::default(),
172        workspace_diagnostics: Vec::new(),
173    })
174    .map_err(|err| {
175        ProgrammaticError::new(format!("failed to serialize audit dead-code: {err}"), 2)
176            .with_code("FALLOW_SERIALIZE_AUDIT_DEAD_CODE")
177            .with_context("audit.deadCode")
178    })?;
179    if let Some(base) = base_snapshot {
180        crate::audit_keys::annotate_dead_code_json(
181            &mut json,
182            &output.output.results,
183            &output.root,
184            &base.dead_code,
185        );
186    }
187    Ok(json)
188}
189
190fn serialize_audit_duplication(
191    output: &DuplicationProgrammaticOutput,
192    base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
193) -> ProgrammaticResult<serde_json::Value> {
194    let mut json = serde_json::to_value(&output.output.report).map_err(|err| {
195        ProgrammaticError::new(format!("failed to serialize audit duplication: {err}"), 2)
196            .with_code("FALLOW_SERIALIZE_AUDIT_DUPLICATION")
197            .with_context("audit.duplication")
198    })?;
199    let root_prefix = format!("{}/", output.root.display());
200    strip_root_prefix(&mut json, &root_prefix);
201    if let Some(base) = base_snapshot {
202        annotate_audit_duplication_json(&mut json, output, &base.dupes);
203    }
204    Ok(json)
205}
206
207fn serialize_audit_complexity(
208    output: &HealthProgrammaticOutput,
209    base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
210) -> ProgrammaticResult<serde_json::Value> {
211    let mut json = serde_json::to_value(&output.report).map_err(|err| {
212        ProgrammaticError::new(format!("failed to serialize audit complexity: {err}"), 2)
213            .with_code("FALLOW_SERIALIZE_AUDIT_COMPLEXITY")
214            .with_context("audit.complexity")
215    })?;
216    let root_prefix = format!("{}/", output.root.display());
217    strip_root_prefix(&mut json, &root_prefix);
218    if let Some(base) = base_snapshot {
219        crate::audit_keys::annotate_health_json(
220            &mut json,
221            &output.report,
222            &output.root,
223            &base.health,
224        );
225    }
226    Ok(json)
227}
228
229fn annotate_audit_duplication_json(
230    json: &mut serde_json::Value,
231    output: &DuplicationProgrammaticOutput,
232    base: &rustc_hash::FxHashSet<String>,
233) {
234    let Some(items) = json
235        .get_mut("clone_groups")
236        .and_then(serde_json::Value::as_array_mut)
237    else {
238        return;
239    };
240    for (item, group) in items.iter_mut().zip(&output.output.report.clone_groups) {
241        if let serde_json::Value::Object(map) = item {
242            let key = crate::audit_keys::dupe_group_key(&group.group, &output.root);
243            map.insert(
244                "introduced".to_string(),
245                serde_json::json!(!base.contains(&key)),
246            );
247        }
248    }
249}
250
251/// Serialize typed dead-code output into the stable JSON compatibility contract.
252///
253/// # Errors
254///
255/// Returns a structured error if the output contract cannot be serialized.
256pub fn serialize_dead_code_programmatic_json(
257    output: DeadCodeProgrammaticOutput,
258) -> ProgrammaticResult<serde_json::Value> {
259    let DeadCodeProgrammaticOutput {
260        output,
261        root,
262        config_fixable: _,
263        envelope_mode,
264        telemetry_analysis_run_id,
265    } = output;
266    serialize_check_programmatic_output(
267        output,
268        &root,
269        envelope_mode,
270        telemetry_analysis_run_id.as_deref(),
271        "dead-code",
272        "FALLOW_SERIALIZE_DEAD_CODE_REPORT",
273    )
274}
275
276/// Serialize typed circular-dependency output into the JSON compatibility contract.
277///
278/// # Errors
279///
280/// Returns a structured error if the output contract cannot be serialized.
281pub fn serialize_circular_dependencies_programmatic_json(
282    output: CircularDependenciesProgrammaticOutput,
283) -> ProgrammaticResult<serde_json::Value> {
284    let CircularDependenciesProgrammaticOutput {
285        output,
286        root,
287        envelope_mode,
288        telemetry_analysis_run_id,
289    } = output;
290    serialize_check_programmatic_output(
291        output,
292        &root,
293        envelope_mode,
294        telemetry_analysis_run_id.as_deref(),
295        "circular-dependencies",
296        "FALLOW_SERIALIZE_CIRCULAR_DEPENDENCIES_REPORT",
297    )
298}
299
300/// Serialize typed boundary-family output into the JSON compatibility contract.
301///
302/// # Errors
303///
304/// Returns a structured error if the output contract cannot be serialized.
305pub fn serialize_boundary_violations_programmatic_json(
306    output: BoundaryViolationsProgrammaticOutput,
307) -> ProgrammaticResult<serde_json::Value> {
308    let BoundaryViolationsProgrammaticOutput {
309        output,
310        root,
311        envelope_mode,
312        telemetry_analysis_run_id,
313    } = output;
314    serialize_check_programmatic_output(
315        output,
316        &root,
317        envelope_mode,
318        telemetry_analysis_run_id.as_deref(),
319        "boundary-violations",
320        "FALLOW_SERIALIZE_BOUNDARY_VIOLATIONS_REPORT",
321    )
322}
323
324fn serialize_check_programmatic_output(
325    output: CheckOutput,
326    root: &Path,
327    envelope_mode: RootEnvelopeMode,
328    telemetry_analysis_run_id: Option<&str>,
329    context: &'static str,
330    code: &'static str,
331) -> ProgrammaticResult<serde_json::Value> {
332    let mut json = serialize_check_json_output(output, envelope_mode, telemetry_analysis_run_id)
333        .map_err(|err| {
334            ProgrammaticError::new(format!("failed to serialize {context} report: {err}"), 2)
335                .with_code(code)
336                .with_context(context)
337        })?;
338    let root_prefix = format!("{}/", root.display());
339    strip_root_prefix(&mut json, &root_prefix);
340    Ok(json)
341}
342
343/// Serialize typed duplication output into the JSON compatibility contract.
344///
345/// # Errors
346///
347/// Returns a structured error if the output contract cannot be serialized.
348pub fn serialize_duplication_programmatic_json(
349    output: DuplicationProgrammaticOutput,
350) -> ProgrammaticResult<serde_json::Value> {
351    let DuplicationProgrammaticOutput {
352        output,
353        root,
354        threshold: _,
355        envelope_mode,
356        telemetry_analysis_run_id,
357    } = output;
358    let mut json =
359        serialize_dupes_json_output(output, envelope_mode, telemetry_analysis_run_id.as_deref())
360            .map_err(|err| {
361                ProgrammaticError::new(format!("failed to serialize duplication report: {err}"), 2)
362                    .with_code("FALLOW_SERIALIZE_DUPLICATION_REPORT")
363                    .with_context("dupes")
364            })?;
365    let root_prefix = format!("{}/", root.display());
366    strip_root_prefix(&mut json, &root_prefix);
367    Ok(json)
368}
369
370/// Serialize typed feature-flag output into the JSON compatibility contract.
371///
372/// # Errors
373///
374/// Returns a structured error if the output contract cannot be serialized.
375pub fn serialize_feature_flags_programmatic_json(
376    output: FeatureFlagsProgrammaticOutput,
377) -> ProgrammaticResult<serde_json::Value> {
378    serialize_feature_flags_json_output(
379        output.output,
380        output.envelope_mode,
381        output.telemetry_analysis_run_id.as_deref(),
382    )
383    .map_err(|err| {
384        ProgrammaticError::new(
385            format!("failed to serialize feature flags report: {err}"),
386            2,
387        )
388        .with_code("FALLOW_SERIALIZE_FEATURE_FLAGS_REPORT")
389        .with_context("feature-flags")
390    })
391}
392
393/// Serialize typed export-trace output into the JSON compatibility contract.
394///
395/// # Errors
396///
397/// Returns a structured error if the trace output cannot be serialized.
398pub fn serialize_trace_export_programmatic_json(
399    output: TraceExportProgrammaticOutput,
400) -> ProgrammaticResult<serde_json::Value> {
401    serialize_trace_programmatic_output(
402        output.output,
403        "export trace",
404        "FALLOW_SERIALIZE_TRACE_EXPORT",
405        "trace_export",
406    )
407}
408
409/// Serialize typed file-trace output into the JSON compatibility contract.
410///
411/// # Errors
412///
413/// Returns a structured error if the trace output cannot be serialized.
414pub fn serialize_trace_file_programmatic_json(
415    output: TraceFileProgrammaticOutput,
416) -> ProgrammaticResult<serde_json::Value> {
417    serialize_trace_programmatic_output(
418        output.output,
419        "file trace",
420        "FALLOW_SERIALIZE_TRACE_FILE",
421        "trace_file",
422    )
423}
424
425/// Serialize typed dependency-trace output into the JSON compatibility contract.
426///
427/// # Errors
428///
429/// Returns a structured error if the trace output cannot be serialized.
430pub fn serialize_trace_dependency_programmatic_json(
431    output: TraceDependencyProgrammaticOutput,
432) -> ProgrammaticResult<serde_json::Value> {
433    serialize_trace_programmatic_output(
434        output.output,
435        "dependency trace",
436        "FALLOW_SERIALIZE_TRACE_DEPENDENCY",
437        "trace_dependency",
438    )
439}
440
441/// Serialize typed clone-trace output into the JSON compatibility contract.
442///
443/// # Errors
444///
445/// Returns a structured error if the trace output cannot be serialized.
446pub fn serialize_trace_clone_programmatic_json(
447    output: TraceCloneProgrammaticOutput,
448) -> ProgrammaticResult<serde_json::Value> {
449    serialize_trace_programmatic_output(
450        output.output,
451        "clone trace",
452        "FALLOW_SERIALIZE_TRACE_CLONE",
453        "trace_clone",
454    )
455}
456
457fn serialize_trace_programmatic_output<T: Serialize>(
458    output: T,
459    context: &'static str,
460    code: &'static str,
461    error_context: &'static str,
462) -> ProgrammaticResult<serde_json::Value> {
463    serde_json::to_value(output).map_err(|err| {
464        ProgrammaticError::new(format!("failed to serialize {context}: {err}"), 2)
465            .with_code(code)
466            .with_context(error_context)
467    })
468}
469
470/// Serialize typed health / complexity output into the JSON compatibility contract.
471///
472/// # Errors
473///
474/// Returns a structured error if the health output contract cannot be serialized.
475pub fn serialize_health_programmatic_json(
476    output: HealthProgrammaticOutput,
477) -> ProgrammaticResult<serde_json::Value> {
478    let HealthProgrammaticOutput {
479        report,
480        grouping,
481        root,
482        elapsed,
483        explain,
484        workspace_diagnostics,
485        next_steps,
486        envelope_mode,
487        telemetry_analysis_run_id,
488    } = output;
489    let (grouped_by, groups) = grouping.map_or((None, None), |grouping| {
490        (
491            group_by_mode_from_label(grouping.mode),
492            Some(grouping.groups),
493        )
494    });
495    serialize_health_report_json(HealthJsonReportInput {
496        report,
497        root: &root,
498        elapsed,
499        explain,
500        grouped_by,
501        groups,
502        workspace_diagnostics,
503        next_steps,
504        envelope_mode,
505        telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
506    })
507    .map_err(|err| {
508        ProgrammaticError::new(format!("failed to serialize health report: {err}"), 2)
509            .with_code("FALLOW_SERIALIZE_HEALTH_REPORT")
510            .with_context("health")
511    })
512}
513
514fn group_by_mode_from_label(label: &str) -> Option<GroupByMode> {
515    match label {
516        "owner" => Some(GroupByMode::Owner),
517        "directory" => Some(GroupByMode::Directory),
518        "package" => Some(GroupByMode::Package),
519        "section" => Some(GroupByMode::Section),
520        _ => None,
521    }
522}