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