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 AUDIT_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 type_aware: None,
68 next_steps,
69 envelope_mode,
70 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
71 })
72 .map_err(|err| {
73 ProgrammaticError::new(format!("failed to serialize combined report: {err}"), 2)
74 .with_code("FALLOW_SERIALIZE_COMBINED_REPORT")
75 .with_context("combined")
76 })
77}
78
79pub fn serialize_decision_surface_programmatic_json(
85 output: DecisionSurfaceProgrammaticOutput,
86) -> ProgrammaticResult<serde_json::Value> {
87 let DecisionSurfaceProgrammaticOutput {
88 surface,
89 elapsed: _,
90 envelope_mode,
91 telemetry_analysis_run_id,
92 } = output;
93 let payload = build_decision_surface_output(&surface);
94 serialize_decision_surface_json_output(
95 payload,
96 envelope_mode,
97 telemetry_analysis_run_id.as_deref(),
98 )
99 .map_err(|err| {
100 ProgrammaticError::new(format!("failed to serialize decision surface: {err}"), 2)
101 .with_code("FALLOW_SERIALIZE_DECISION_SURFACE")
102 .with_context("decision-surface")
103 })
104}
105
106pub fn serialize_audit_programmatic_json(
112 output: AuditProgrammaticOutput,
113) -> ProgrammaticResult<serde_json::Value> {
114 let base_snapshot = output.base_snapshot.as_ref();
115 let dead_code = output
116 .dead_code
117 .as_ref()
118 .map(|dead_code| serialize_audit_dead_code(dead_code, base_snapshot))
119 .transpose()?;
120 let duplication = output
121 .duplication
122 .as_ref()
123 .map(|duplication| serialize_audit_duplication(duplication, base_snapshot))
124 .transpose()?;
125 let complexity = output
126 .complexity
127 .as_ref()
128 .map(|complexity| serialize_audit_complexity(complexity, base_snapshot))
129 .transpose()?;
130
131 crate::serialize_audit_json(
132 crate::AuditJsonOutputInput {
133 header: crate::AuditJsonHeaderInput {
134 schema_version: SchemaVersion(AUDIT_SCHEMA_VERSION),
135 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
136 verdict: output.verdict,
137 changed_files_count: u32::try_from(output.changed_files_count).unwrap_or(u32::MAX),
138 base_ref: output.base_ref,
139 base_description: output.base_description,
140 head_sha: output.head_sha,
141 elapsed_ms: ElapsedMs(
142 u64::try_from(output.elapsed.as_millis()).unwrap_or(u64::MAX),
143 ),
144 base_snapshot_skipped: output.base_snapshot_skipped,
145 summary: output.summary,
146 attribution: output.attribution,
147 },
148 meta: None,
149 dead_code,
150 duplication,
151 complexity,
152 next_steps: output.next_steps,
153 },
154 output.envelope_mode,
155 output.telemetry_analysis_run_id.as_deref(),
156 )
157 .map_err(|err| {
158 ProgrammaticError::new(format!("failed to serialize audit report: {err}"), 2)
159 .with_code("FALLOW_SERIALIZE_AUDIT_REPORT")
160 .with_context("audit")
161 })
162}
163
164fn serialize_audit_dead_code(
165 output: &DeadCodeProgrammaticOutput,
166 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
167) -> ProgrammaticResult<serde_json::Value> {
168 let mut json = crate::serialize_check_json_payload(crate::CheckJsonPayloadInput {
169 results: &output.output.results,
170 root: &output.root,
171 elapsed: Duration::from_millis(output.output.elapsed_ms.0),
172 config_fixable: output.config_fixable,
173 extras: crate::CheckJsonExtraOutputs::default(),
174 workspace_diagnostics: Vec::new(),
175 })
176 .map_err(|err| {
177 ProgrammaticError::new(format!("failed to serialize audit dead-code: {err}"), 2)
178 .with_code("FALLOW_SERIALIZE_AUDIT_DEAD_CODE")
179 .with_context("audit.deadCode")
180 })?;
181 if let Some(base) = base_snapshot {
182 if has_persisted_introduced_flags(&json) {
183 crate::audit_keys::annotate_stale_suppressions_json(
184 &mut json,
185 &output.output.results,
186 &output.root,
187 &base.dead_code,
188 );
189 } else {
190 crate::audit_keys::annotate_dead_code_json(
191 &mut json,
192 &output.output.results,
193 &output.root,
194 &base.dead_code,
195 );
196 }
197 }
198 Ok(json)
199}
200
201fn serialize_audit_duplication(
202 output: &DuplicationProgrammaticOutput,
203 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
204) -> ProgrammaticResult<serde_json::Value> {
205 let mut json = serde_json::to_value(&output.output.report).map_err(|err| {
206 ProgrammaticError::new(format!("failed to serialize audit duplication: {err}"), 2)
207 .with_code("FALLOW_SERIALIZE_AUDIT_DUPLICATION")
208 .with_context("audit.duplication")
209 })?;
210 let root_prefix = format!("{}/", output.root.display());
211 strip_root_prefix(&mut json, &root_prefix);
212 if let Some(base) = base_snapshot
213 && !has_persisted_introduced_flags(&json)
214 {
215 annotate_audit_duplication_json(&mut json, output, &base.dupes);
216 }
217 Ok(json)
218}
219
220fn serialize_audit_complexity(
221 output: &HealthProgrammaticOutput,
222 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
223) -> ProgrammaticResult<serde_json::Value> {
224 let mut json = serde_json::to_value(&output.report).map_err(|err| {
225 ProgrammaticError::new(format!("failed to serialize audit complexity: {err}"), 2)
226 .with_code("FALLOW_SERIALIZE_AUDIT_COMPLEXITY")
227 .with_context("audit.complexity")
228 })?;
229 let root_prefix = format!("{}/", output.root.display());
230 strip_root_prefix(&mut json, &root_prefix);
231 if let Some(base) = base_snapshot {
232 crate::audit_keys::annotate_health_json(
233 &mut json,
234 &output.report,
235 &output.root,
236 &base.health,
237 );
238 }
239 Ok(json)
240}
241
242fn has_persisted_introduced_flags(json: &serde_json::Value) -> bool {
243 json.as_object().is_some_and(|object| {
244 object.values().any(|value| {
245 value
246 .as_array()
247 .is_some_and(|items| items.iter().any(|item| item.get("introduced").is_some()))
248 })
249 })
250}
251
252fn annotate_audit_duplication_json(
253 json: &mut serde_json::Value,
254 output: &DuplicationProgrammaticOutput,
255 base: &rustc_hash::FxHashSet<String>,
256) {
257 let Some(items) = json
258 .get_mut("clone_groups")
259 .and_then(serde_json::Value::as_array_mut)
260 else {
261 return;
262 };
263 for (item, group) in items.iter_mut().zip(&output.output.report.clone_groups) {
264 if let serde_json::Value::Object(map) = item {
265 let key = crate::audit_keys::dupe_group_key(&group.group, &output.root);
266 map.insert(
267 "introduced".to_string(),
268 serde_json::json!(!base.contains(&key)),
269 );
270 }
271 }
272}
273
274pub fn serialize_dead_code_programmatic_json(
280 output: DeadCodeProgrammaticOutput,
281) -> ProgrammaticResult<serde_json::Value> {
282 let DeadCodeProgrammaticOutput {
283 output,
284 root,
285 config_fixable: _,
286 envelope_mode,
287 telemetry_analysis_run_id,
288 } = output;
289 serialize_check_programmatic_output(
290 output,
291 &root,
292 envelope_mode,
293 telemetry_analysis_run_id.as_deref(),
294 "dead-code",
295 "FALLOW_SERIALIZE_DEAD_CODE_REPORT",
296 )
297}
298
299pub fn serialize_circular_dependencies_programmatic_json(
305 output: CircularDependenciesProgrammaticOutput,
306) -> ProgrammaticResult<serde_json::Value> {
307 let CircularDependenciesProgrammaticOutput {
308 output,
309 root,
310 envelope_mode,
311 telemetry_analysis_run_id,
312 } = output;
313 serialize_check_programmatic_output(
314 output,
315 &root,
316 envelope_mode,
317 telemetry_analysis_run_id.as_deref(),
318 "circular-dependencies",
319 "FALLOW_SERIALIZE_CIRCULAR_DEPENDENCIES_REPORT",
320 )
321}
322
323pub fn serialize_boundary_violations_programmatic_json(
329 output: BoundaryViolationsProgrammaticOutput,
330) -> ProgrammaticResult<serde_json::Value> {
331 let BoundaryViolationsProgrammaticOutput {
332 output,
333 root,
334 envelope_mode,
335 telemetry_analysis_run_id,
336 } = output;
337 serialize_check_programmatic_output(
338 output,
339 &root,
340 envelope_mode,
341 telemetry_analysis_run_id.as_deref(),
342 "boundary-violations",
343 "FALLOW_SERIALIZE_BOUNDARY_VIOLATIONS_REPORT",
344 )
345}
346
347fn serialize_check_programmatic_output(
348 output: CheckOutput,
349 root: &Path,
350 envelope_mode: RootEnvelopeMode,
351 telemetry_analysis_run_id: Option<&str>,
352 context: &'static str,
353 code: &'static str,
354) -> ProgrammaticResult<serde_json::Value> {
355 let mut json = serialize_check_json_output(output, envelope_mode, telemetry_analysis_run_id)
356 .map_err(|err| {
357 ProgrammaticError::new(format!("failed to serialize {context} report: {err}"), 2)
358 .with_code(code)
359 .with_context(context)
360 })?;
361 let root_prefix = format!("{}/", root.display());
362 strip_root_prefix(&mut json, &root_prefix);
363 Ok(json)
364}
365
366pub fn serialize_duplication_programmatic_json(
372 output: DuplicationProgrammaticOutput,
373) -> ProgrammaticResult<serde_json::Value> {
374 let DuplicationProgrammaticOutput {
375 output,
376 root,
377 threshold: _,
378 envelope_mode,
379 telemetry_analysis_run_id,
380 } = output;
381 let mut json =
382 serialize_dupes_json_output(output, envelope_mode, telemetry_analysis_run_id.as_deref())
383 .map_err(|err| {
384 ProgrammaticError::new(format!("failed to serialize duplication report: {err}"), 2)
385 .with_code("FALLOW_SERIALIZE_DUPLICATION_REPORT")
386 .with_context("dupes")
387 })?;
388 let root_prefix = format!("{}/", root.display());
389 strip_root_prefix(&mut json, &root_prefix);
390 Ok(json)
391}
392
393pub fn serialize_feature_flags_programmatic_json(
399 output: FeatureFlagsProgrammaticOutput,
400) -> ProgrammaticResult<serde_json::Value> {
401 serialize_feature_flags_json_output(
402 output.output,
403 output.envelope_mode,
404 output.telemetry_analysis_run_id.as_deref(),
405 )
406 .map_err(|err| {
407 ProgrammaticError::new(
408 format!("failed to serialize feature flags report: {err}"),
409 2,
410 )
411 .with_code("FALLOW_SERIALIZE_FEATURE_FLAGS_REPORT")
412 .with_context("feature-flags")
413 })
414}
415
416pub fn serialize_trace_export_programmatic_json(
422 output: TraceExportProgrammaticOutput,
423) -> ProgrammaticResult<serde_json::Value> {
424 serialize_trace_programmatic_output(
425 output.output,
426 "export trace",
427 "FALLOW_SERIALIZE_TRACE_EXPORT",
428 "trace_export",
429 )
430}
431
432pub fn serialize_trace_file_programmatic_json(
438 output: TraceFileProgrammaticOutput,
439) -> ProgrammaticResult<serde_json::Value> {
440 serialize_trace_programmatic_output(
441 output.output,
442 "file trace",
443 "FALLOW_SERIALIZE_TRACE_FILE",
444 "trace_file",
445 )
446}
447
448pub fn serialize_trace_dependency_programmatic_json(
454 output: TraceDependencyProgrammaticOutput,
455) -> ProgrammaticResult<serde_json::Value> {
456 serialize_trace_programmatic_output(
457 output.output,
458 "dependency trace",
459 "FALLOW_SERIALIZE_TRACE_DEPENDENCY",
460 "trace_dependency",
461 )
462}
463
464pub fn serialize_trace_clone_programmatic_json(
470 output: TraceCloneProgrammaticOutput,
471) -> ProgrammaticResult<serde_json::Value> {
472 serialize_trace_programmatic_output(
473 output.output,
474 "clone trace",
475 "FALLOW_SERIALIZE_TRACE_CLONE",
476 "trace_clone",
477 )
478}
479
480fn serialize_trace_programmatic_output<T: Serialize>(
481 output: T,
482 context: &'static str,
483 code: &'static str,
484 error_context: &'static str,
485) -> ProgrammaticResult<serde_json::Value> {
486 serde_json::to_value(output).map_err(|err| {
487 ProgrammaticError::new(format!("failed to serialize {context}: {err}"), 2)
488 .with_code(code)
489 .with_context(error_context)
490 })
491}
492
493pub fn serialize_health_programmatic_json(
499 output: HealthProgrammaticOutput,
500) -> ProgrammaticResult<serde_json::Value> {
501 let HealthProgrammaticOutput {
502 report,
503 grouping,
504 root,
505 elapsed,
506 explain,
507 workspace_diagnostics,
508 next_steps,
509 envelope_mode,
510 telemetry_analysis_run_id,
511 } = output;
512 let (grouped_by, groups) = grouping.map_or((None, None), |grouping| {
513 (
514 group_by_mode_from_label(grouping.mode),
515 Some(grouping.groups),
516 )
517 });
518 serialize_health_report_json(HealthJsonReportInput {
519 report,
520 root: &root,
521 elapsed,
522 explain,
523 type_aware: None,
524 grouped_by,
525 groups,
526 workspace_diagnostics,
527 next_steps,
528 envelope_mode,
529 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
530 })
531 .map_err(|err| {
532 ProgrammaticError::new(format!("failed to serialize health report: {err}"), 2)
533 .with_code("FALLOW_SERIALIZE_HEALTH_REPORT")
534 .with_context("health")
535 })
536}
537
538fn group_by_mode_from_label(label: &str) -> Option<GroupByMode> {
539 match label {
540 "owner" => Some(GroupByMode::Owner),
541 "directory" => Some(GroupByMode::Directory),
542 "package" => Some(GroupByMode::Package),
543 "section" => Some(GroupByMode::Section),
544 _ => None,
545 }
546}