1use 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
31pub 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
78pub 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
105pub 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
251pub 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
276pub 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
300pub 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
343pub 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
370pub 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
393pub 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
409pub 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
425pub 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
441pub 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
470pub 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}