1use 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
31pub 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
58pub 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
204pub 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
228pub 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
252pub 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
295pub 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
322pub 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
345pub 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
361pub 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
377pub 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
393pub 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
422pub 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}