1use crate::{
8 ProgrammaticError,
9 runtime::{
10 AuditProgrammaticOutput, BoundaryViolationsProgrammaticOutput,
11 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput,
12 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput,
13 DuplicationProgrammaticOutput, FeatureFlagsProgrammaticOutput, HealthJsonReportInput,
14 HealthProgrammaticOutput, TraceCloneProgrammaticOutput, TraceDependencyProgrammaticOutput,
15 TraceErrorProgrammaticOutput, TraceExportProgrammaticOutput, TraceFileProgrammaticOutput,
16 TraceImportPathProgrammaticOutput, serialize_health_report_json,
17 },
18};
19use fallow_output::{
20 AUDIT_SCHEMA_VERSION, CheckOutput, GroupByMode, build_decision_surface_output,
21 serialize_check_json_output, serialize_decision_surface_json_output,
22 serialize_dupes_json_output, serialize_feature_flags_json_output, strip_root_prefix,
23};
24use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
25use fallow_types::workspace::{WorkspaceDiagnostic, merge_workspace_diagnostics};
26use serde::Serialize;
27use std::path::Path;
28use std::time::Duration;
29
30type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
31
32pub fn serialize_combined_programmatic_json(
38 output: CombinedProgrammaticOutput,
39) -> ProgrammaticResult<serde_json::Value> {
40 let CombinedProgrammaticOutput {
41 dead_code,
42 duplication,
43 health,
44 root,
45 elapsed,
46 explain,
47 next_steps,
48 telemetry_analysis_run_id,
49 request_outcomes,
50 } = output;
51 let workspace_diagnostics =
52 combined_workspace_diagnostics(dead_code.as_ref(), health.as_ref(), duplication.as_ref());
53 crate::serialize_combined_json(crate::CombinedJsonOutputInput {
54 gate_outcomes: None,
55 request_outcomes,
56 check: dead_code
57 .as_ref()
58 .map(|dead_code| crate::CombinedCheckJsonSection {
59 results: &dead_code.output.results,
60 root: &dead_code.root,
61 elapsed: Duration::from_millis(dead_code.output.elapsed_ms.0),
62 config_fixable: dead_code.config_fixable,
63 extras: crate::CheckJsonExtraOutputs::default(),
64 }),
65 dupes: duplication
66 .as_ref()
67 .map(|duplication| &duplication.output.report),
68 health: health.as_ref().map(|health| &health.report),
69 root: &root,
70 elapsed,
71 explain,
72 type_aware: None,
73 workspace_diagnostics,
74 next_steps,
75 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
76 })
77 .map_err(|err| {
78 ProgrammaticError::new(format!("failed to serialize combined report: {err}"), 2)
79 .with_code("FALLOW_SERIALIZE_COMBINED_REPORT")
80 .with_context("combined")
81 })
82}
83
84fn combined_workspace_diagnostics(
99 dead_code: Option<&DeadCodeProgrammaticOutput>,
100 health: Option<&HealthProgrammaticOutput>,
101 duplication: Option<&DuplicationProgrammaticOutput>,
102) -> Vec<WorkspaceDiagnostic> {
103 let merged = merge_workspace_diagnostics(
104 dead_code.map_or_else(Vec::new, |dead_code| {
105 dead_code.output.workspace_diagnostics.clone()
106 }),
107 health.map_or_else(Vec::new, |health| health.workspace_diagnostics.clone()),
108 );
109 merge_workspace_diagnostics(
110 merged,
111 duplication.map_or_else(Vec::new, |duplication| {
112 duplication.output.workspace_diagnostics.clone()
113 }),
114 )
115}
116
117pub fn serialize_decision_surface_programmatic_json(
123 output: DecisionSurfaceProgrammaticOutput,
124) -> ProgrammaticResult<serde_json::Value> {
125 let DecisionSurfaceProgrammaticOutput {
126 surface,
127 elapsed: _,
128 telemetry_analysis_run_id,
129 } = output;
130 let payload = build_decision_surface_output(&surface);
131 serialize_decision_surface_json_output(payload, telemetry_analysis_run_id.as_deref()).map_err(
132 |err| {
133 ProgrammaticError::new(format!("failed to serialize decision surface: {err}"), 2)
134 .with_code("FALLOW_SERIALIZE_DECISION_SURFACE")
135 .with_context("decision-surface")
136 },
137 )
138}
139
140pub fn serialize_audit_programmatic_json(
146 output: AuditProgrammaticOutput,
147) -> ProgrammaticResult<serde_json::Value> {
148 let base_snapshot = output.base_snapshot.as_ref();
149 let dead_code = output
150 .dead_code
151 .as_ref()
152 .map(|dead_code| serialize_audit_dead_code(dead_code, base_snapshot))
153 .transpose()?;
154 let duplication = output
155 .duplication
156 .as_ref()
157 .map(|duplication| serialize_audit_duplication(duplication, base_snapshot))
158 .transpose()?;
159 let complexity = output
160 .complexity
161 .as_ref()
162 .map(|complexity| serialize_audit_complexity(complexity, base_snapshot))
163 .transpose()?;
164
165 crate::serialize_audit_json(
166 crate::AuditJsonOutputInput {
167 gate_outcomes: None,
168 header: crate::AuditJsonHeaderInput {
169 schema_version: SchemaVersion(AUDIT_SCHEMA_VERSION),
170 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
171 verdict: output.verdict,
172 changed_files_count: u32::try_from(output.changed_files_count).unwrap_or(u32::MAX),
173 base_ref: output.base_ref,
174 base_description: output.base_description,
175 head_sha: output.head_sha,
176 elapsed_ms: ElapsedMs(
177 u64::try_from(output.elapsed.as_millis()).unwrap_or(u64::MAX),
178 ),
179 base_snapshot_skipped: output.base_snapshot_skipped,
180 summary: output.summary,
181 attribution: output.attribution,
182 },
183 meta: None,
184 dead_code,
185 duplication,
186 complexity,
187 next_steps: output.next_steps,
188 },
189 output.telemetry_analysis_run_id.as_deref(),
190 )
191 .map_err(|err| {
192 ProgrammaticError::new(format!("failed to serialize audit report: {err}"), 2)
193 .with_code("FALLOW_SERIALIZE_AUDIT_REPORT")
194 .with_context("audit")
195 })
196}
197
198fn serialize_audit_dead_code(
209 output: &DeadCodeProgrammaticOutput,
210 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
211) -> ProgrammaticResult<serde_json::Value> {
212 let mut json = crate::serialize_check_json_payload(crate::CheckJsonPayloadInput {
213 results: &output.output.results,
214 root: &output.root,
215 elapsed: Duration::from_millis(output.output.elapsed_ms.0),
216 config_fixable: output.config_fixable,
217 extras: crate::CheckJsonExtraOutputs::default(),
218 workspace_diagnostics: output.output.workspace_diagnostics.clone(),
219 })
220 .map_err(|err| {
221 ProgrammaticError::new(format!("failed to serialize audit dead-code: {err}"), 2)
222 .with_code("FALLOW_SERIALIZE_AUDIT_DEAD_CODE")
223 .with_context("audit.deadCode")
224 })?;
225 if let Some(base) = base_snapshot {
226 if has_persisted_introduced_flags(&json) {
227 crate::audit_keys::annotate_stale_suppressions_json(
228 &mut json,
229 &output.output.results,
230 &output.root,
231 &base.dead_code,
232 );
233 } else {
234 crate::audit_keys::annotate_dead_code_json(
235 &mut json,
236 &output.output.results,
237 &output.root,
238 &base.dead_code,
239 );
240 }
241 }
242 Ok(json)
243}
244
245fn serialize_audit_duplication(
246 output: &DuplicationProgrammaticOutput,
247 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
248) -> ProgrammaticResult<serde_json::Value> {
249 let mut json = serde_json::to_value(&output.output.report).map_err(|err| {
250 ProgrammaticError::new(format!("failed to serialize audit duplication: {err}"), 2)
251 .with_code("FALLOW_SERIALIZE_AUDIT_DUPLICATION")
252 .with_context("audit.duplication")
253 })?;
254 let root_prefix = format!("{}/", output.root.display());
255 strip_root_prefix(&mut json, &root_prefix);
256 if let Some(base) = base_snapshot
257 && !has_persisted_introduced_flags(&json)
258 {
259 annotate_audit_duplication_json(&mut json, output, &base.dupes);
260 }
261 Ok(json)
262}
263
264fn serialize_audit_complexity(
265 output: &HealthProgrammaticOutput,
266 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
267) -> ProgrammaticResult<serde_json::Value> {
268 let mut json = serde_json::to_value(&output.report).map_err(|err| {
269 ProgrammaticError::new(format!("failed to serialize audit complexity: {err}"), 2)
270 .with_code("FALLOW_SERIALIZE_AUDIT_COMPLEXITY")
271 .with_context("audit.complexity")
272 })?;
273 let root_prefix = format!("{}/", output.root.display());
274 strip_root_prefix(&mut json, &root_prefix);
275 if let Some(base) = base_snapshot {
276 crate::audit_keys::annotate_health_json(
277 &mut json,
278 &output.report,
279 &output.root,
280 &base.health,
281 );
282 }
283 Ok(json)
284}
285
286fn has_persisted_introduced_flags(json: &serde_json::Value) -> bool {
287 json.as_object().is_some_and(|object| {
288 object.values().any(|value| {
289 value
290 .as_array()
291 .is_some_and(|items| items.iter().any(|item| item.get("introduced").is_some()))
292 })
293 })
294}
295
296fn annotate_audit_duplication_json(
297 json: &mut serde_json::Value,
298 output: &DuplicationProgrammaticOutput,
299 base: &rustc_hash::FxHashSet<String>,
300) {
301 let Some(items) = json
302 .get_mut("clone_groups")
303 .and_then(serde_json::Value::as_array_mut)
304 else {
305 return;
306 };
307 for (item, group) in items.iter_mut().zip(&output.output.report.clone_groups) {
308 if let serde_json::Value::Object(map) = item {
309 let key = crate::audit_keys::dupe_group_key(&group.group, &output.root);
310 map.insert(
311 "introduced".to_string(),
312 serde_json::json!(!base.contains(&key)),
313 );
314 }
315 }
316}
317
318pub fn serialize_dead_code_programmatic_json(
324 output: DeadCodeProgrammaticOutput,
325) -> ProgrammaticResult<serde_json::Value> {
326 let DeadCodeProgrammaticOutput {
327 output,
328 root,
329 config_fixable: _,
330 telemetry_analysis_run_id,
331 } = output;
332 serialize_check_programmatic_output(
333 output,
334 &root,
335 telemetry_analysis_run_id.as_deref(),
336 "dead-code",
337 "FALLOW_SERIALIZE_DEAD_CODE_REPORT",
338 )
339}
340
341pub fn serialize_circular_dependencies_programmatic_json(
347 output: CircularDependenciesProgrammaticOutput,
348) -> ProgrammaticResult<serde_json::Value> {
349 let CircularDependenciesProgrammaticOutput {
350 output,
351 root,
352 telemetry_analysis_run_id,
353 } = output;
354 serialize_check_programmatic_output(
355 output,
356 &root,
357 telemetry_analysis_run_id.as_deref(),
358 "circular-dependencies",
359 "FALLOW_SERIALIZE_CIRCULAR_DEPENDENCIES_REPORT",
360 )
361}
362
363pub fn serialize_boundary_violations_programmatic_json(
369 output: BoundaryViolationsProgrammaticOutput,
370) -> ProgrammaticResult<serde_json::Value> {
371 let BoundaryViolationsProgrammaticOutput {
372 output,
373 root,
374 telemetry_analysis_run_id,
375 } = output;
376 serialize_check_programmatic_output(
377 output,
378 &root,
379 telemetry_analysis_run_id.as_deref(),
380 "boundary-violations",
381 "FALLOW_SERIALIZE_BOUNDARY_VIOLATIONS_REPORT",
382 )
383}
384
385fn serialize_check_programmatic_output(
386 output: CheckOutput,
387 root: &Path,
388 telemetry_analysis_run_id: Option<&str>,
389 context: &'static str,
390 code: &'static str,
391) -> ProgrammaticResult<serde_json::Value> {
392 let mut json =
393 serialize_check_json_output(output, telemetry_analysis_run_id).map_err(|err| {
394 ProgrammaticError::new(format!("failed to serialize {context} report: {err}"), 2)
395 .with_code(code)
396 .with_context(context)
397 })?;
398 let root_prefix = format!("{}/", root.display());
399 strip_root_prefix(&mut json, &root_prefix);
400 Ok(json)
401}
402
403pub fn serialize_duplication_programmatic_json(
409 output: DuplicationProgrammaticOutput,
410) -> ProgrammaticResult<serde_json::Value> {
411 let DuplicationProgrammaticOutput {
412 output,
413 root,
414 threshold: _,
415 telemetry_analysis_run_id,
416 } = output;
417 let mut json = serialize_dupes_json_output(output, telemetry_analysis_run_id.as_deref())
418 .map_err(|err| {
419 ProgrammaticError::new(format!("failed to serialize duplication report: {err}"), 2)
420 .with_code("FALLOW_SERIALIZE_DUPLICATION_REPORT")
421 .with_context("dupes")
422 })?;
423 let root_prefix = format!("{}/", root.display());
424 strip_root_prefix(&mut json, &root_prefix);
425 Ok(json)
426}
427
428pub fn serialize_feature_flags_programmatic_json(
434 output: FeatureFlagsProgrammaticOutput,
435) -> ProgrammaticResult<serde_json::Value> {
436 serialize_feature_flags_json_output(output.output, output.telemetry_analysis_run_id.as_deref())
437 .map_err(|err| {
438 ProgrammaticError::new(
439 format!("failed to serialize feature flags report: {err}"),
440 2,
441 )
442 .with_code("FALLOW_SERIALIZE_FEATURE_FLAGS_REPORT")
443 .with_context("feature-flags")
444 })
445}
446
447pub fn serialize_trace_export_programmatic_json(
453 output: TraceExportProgrammaticOutput,
454) -> ProgrammaticResult<serde_json::Value> {
455 serialize_trace_programmatic_output(
456 output.output,
457 "export trace",
458 "FALLOW_SERIALIZE_TRACE_EXPORT",
459 "trace_export",
460 )
461}
462
463pub fn serialize_trace_file_programmatic_json(
469 output: TraceFileProgrammaticOutput,
470) -> ProgrammaticResult<serde_json::Value> {
471 serialize_trace_programmatic_output(
472 output.output,
473 "file trace",
474 "FALLOW_SERIALIZE_TRACE_FILE",
475 "trace_file",
476 )
477}
478
479pub fn serialize_trace_import_path_programmatic_json(
485 output: TraceImportPathProgrammaticOutput,
486) -> ProgrammaticResult<serde_json::Value> {
487 serialize_trace_programmatic_output(
488 output.output,
489 "import path trace",
490 "FALLOW_SERIALIZE_TRACE_IMPORT_PATH",
491 "trace_import_path",
492 )
493}
494
495pub fn serialize_trace_dependency_programmatic_json(
501 output: TraceDependencyProgrammaticOutput,
502) -> ProgrammaticResult<serde_json::Value> {
503 serialize_trace_programmatic_output(
504 output.output,
505 "dependency trace",
506 "FALLOW_SERIALIZE_TRACE_DEPENDENCY",
507 "trace_dependency",
508 )
509}
510
511pub fn serialize_trace_error_programmatic_json(
517 output: TraceErrorProgrammaticOutput,
518) -> ProgrammaticResult<serde_json::Value> {
519 serialize_trace_programmatic_output(
520 output.output,
521 "stack-trace resolution",
522 "FALLOW_SERIALIZE_TRACE_ERROR",
523 "trace_error",
524 )
525}
526
527pub fn serialize_trace_clone_programmatic_json(
533 output: TraceCloneProgrammaticOutput,
534) -> ProgrammaticResult<serde_json::Value> {
535 serialize_trace_programmatic_output(
536 output.output,
537 "clone trace",
538 "FALLOW_SERIALIZE_TRACE_CLONE",
539 "trace_clone",
540 )
541}
542
543fn serialize_trace_programmatic_output<T: Serialize>(
544 output: T,
545 context: &'static str,
546 code: &'static str,
547 error_context: &'static str,
548) -> ProgrammaticResult<serde_json::Value> {
549 serde_json::to_value(output).map_err(|err| {
550 ProgrammaticError::new(format!("failed to serialize {context}: {err}"), 2)
551 .with_code(code)
552 .with_context(error_context)
553 })
554}
555
556pub fn serialize_health_programmatic_json(
562 output: HealthProgrammaticOutput,
563) -> ProgrammaticResult<serde_json::Value> {
564 let HealthProgrammaticOutput {
565 report,
566 grouping,
567 root,
568 elapsed,
569 explain,
570 workspace_diagnostics,
571 next_steps,
572 telemetry_analysis_run_id,
573 request_outcomes,
574 } = output;
575 let (grouped_by, groups) = grouping.map_or((None, None), |grouping| {
576 (
577 group_by_mode_from_label(grouping.mode),
578 Some(grouping.groups),
579 )
580 });
581 serialize_health_report_json(HealthJsonReportInput {
582 gate_outcomes: None,
583 request_outcomes,
584 report,
585 root: &root,
586 elapsed,
587 explain,
588 type_aware: None,
589 grouped_by,
590 groups,
591 workspace_diagnostics,
592 next_steps,
593 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
594 })
595 .map_err(|err| {
596 ProgrammaticError::new(format!("failed to serialize health report: {err}"), 2)
597 .with_code("FALLOW_SERIALIZE_HEALTH_REPORT")
598 .with_context("health")
599 })
600}
601
602fn group_by_mode_from_label(label: &str) -> Option<GroupByMode> {
603 match label {
604 "owner" => Some(GroupByMode::Owner),
605 "directory" => Some(GroupByMode::Directory),
606 "package" => Some(GroupByMode::Package),
607 "section" => Some(GroupByMode::Section),
608 _ => None,
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::{serialize_audit_dead_code, serialize_combined_programmatic_json};
615 use crate::DupesReportPayload;
616 use crate::runtime::{
617 CombinedProgrammaticOutput, DeadCodeProgrammaticOutput, DuplicationProgrammaticOutput,
618 HealthProgrammaticOutput,
619 };
620 use fallow_output::{
621 CHECK_SCHEMA_VERSION, CheckOutputInput, DUPES_SCHEMA_VERSION, DupesOutputInput,
622 HealthReport, build_check_output, build_dupes_output,
623 };
624 use fallow_types::duplicates::DuplicationReport;
625 use fallow_types::results::AnalysisResults;
626 use fallow_types::workspace::{WorkspaceDiagnostic, WorkspaceDiagnosticKind};
627 use std::path::Path;
628 use std::time::Duration;
629
630 fn dead_code_output(
631 root: &Path,
632 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
633 ) -> DeadCodeProgrammaticOutput {
634 DeadCodeProgrammaticOutput {
635 output: build_check_output(CheckOutputInput {
636 schema_version: CHECK_SCHEMA_VERSION,
637 version: "0.0.0-test".to_owned(),
638 elapsed: Duration::ZERO,
639 results: AnalysisResults::default(),
640 config_fixable: false,
641 meta: None,
642 workspace_diagnostics,
643 next_steps: Vec::new(),
644 }),
645 root: root.to_path_buf(),
646 config_fixable: false,
647 telemetry_analysis_run_id: None,
648 }
649 }
650
651 #[test]
655 fn audit_dead_code_section_carries_workspace_diagnostics_root_relative_or_omits_them() {
656 let root = Path::new("/project");
657 let carried = serialize_audit_dead_code(
658 &dead_code_output(
659 root,
660 vec![WorkspaceDiagnostic::new(
661 root,
662 root.join("package.json"),
663 WorkspaceDiagnosticKind::BunLockbOverrideResolutionSkipped,
664 )],
665 ),
666 None,
667 )
668 .expect("audit dead-code JSON");
669 assert_eq!(
670 carried["workspace_diagnostics"][0]["kind"],
671 "bun-lockb-override-resolution-skipped"
672 );
673 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
674
675 let empty = serialize_audit_dead_code(&dead_code_output(root, Vec::new()), None)
676 .expect("audit dead-code JSON");
677 assert!(
678 empty.get("workspace_diagnostics").is_none(),
679 "an empty list is omitted from the audit dead-code section: {empty}"
680 );
681 }
682
683 fn health_output(
684 root: &Path,
685 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
686 ) -> HealthProgrammaticOutput {
687 HealthProgrammaticOutput {
688 report: HealthReport::default(),
689 grouping: None,
690 root: root.to_path_buf(),
691 elapsed: Duration::ZERO,
692 explain: false,
693 workspace_diagnostics,
694 next_steps: Vec::new(),
695 telemetry_analysis_run_id: None,
696 request_outcomes: None,
697 }
698 }
699
700 fn duplication_output(
701 root: &Path,
702 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
703 ) -> DuplicationProgrammaticOutput {
704 DuplicationProgrammaticOutput {
705 output: build_dupes_output(DupesOutputInput {
706 gate_outcomes: None,
707 request_outcomes: None,
708 baseline_staleness: None,
709 schema_version: DUPES_SCHEMA_VERSION,
710 version: "0.0.0-test".to_owned(),
711 elapsed: Duration::ZERO,
712 report: DupesReportPayload::from_report(&DuplicationReport::default()),
713 clone_groups_shown: 0,
714 clone_groups_omitted: 0,
715 clone_families_shown: 0,
716 clone_families_omitted: 0,
717 grouped_by: None,
718 total_issues: None,
719 groups: None,
720 meta: None,
721 workspace_diagnostics,
722 next_steps: Vec::new(),
723 }),
724 root: root.to_path_buf(),
725 threshold: 0.0,
726 telemetry_analysis_run_id: None,
727 }
728 }
729
730 fn combined_output(
731 root: &Path,
732 dead_code: Option<DeadCodeProgrammaticOutput>,
733 health: Option<HealthProgrammaticOutput>,
734 ) -> CombinedProgrammaticOutput {
735 combined_output_with_duplication(root, dead_code, health, None)
736 }
737
738 fn combined_output_with_duplication(
739 root: &Path,
740 dead_code: Option<DeadCodeProgrammaticOutput>,
741 health: Option<HealthProgrammaticOutput>,
742 duplication: Option<DuplicationProgrammaticOutput>,
743 ) -> CombinedProgrammaticOutput {
744 CombinedProgrammaticOutput {
745 dead_code,
746 duplication,
747 health,
748 root: root.to_path_buf(),
749 elapsed: Duration::ZERO,
750 explain: false,
751 next_steps: Vec::new(),
752 telemetry_analysis_run_id: None,
753 request_outcomes: None,
754 }
755 }
756
757 fn bun_lockb_diagnostic(root: &Path) -> WorkspaceDiagnostic {
758 WorkspaceDiagnostic::new(
759 root,
760 root.join("package.json"),
761 WorkspaceDiagnosticKind::BunLockbOverrideResolutionSkipped,
762 )
763 }
764
765 fn large_file_diagnostic(root: &Path, relative: &str) -> WorkspaceDiagnostic {
766 WorkspaceDiagnostic::new(
767 root,
768 root.join(relative),
769 WorkspaceDiagnosticKind::SkippedLargeFile {
770 size_bytes: 6_000_000,
771 },
772 )
773 }
774
775 fn root_kinds(document: &serde_json::Value) -> Vec<String> {
776 document["workspace_diagnostics"]
777 .as_array()
778 .cloned()
779 .unwrap_or_default()
780 .iter()
781 .map(|diagnostic| diagnostic["kind"].as_str().unwrap_or_default().to_owned())
782 .collect()
783 }
784
785 #[test]
789 fn combined_programmatic_root_carries_workspace_diagnostics_or_omits_them() {
790 let root = Path::new("/project");
791 let carried = serialize_combined_programmatic_json(combined_output(
792 root,
793 Some(dead_code_output(root, vec![bun_lockb_diagnostic(root)])),
794 None,
795 ))
796 .expect("combined JSON");
797 assert_eq!(
798 carried["workspace_diagnostics"][0]["kind"],
799 "bun-lockb-override-resolution-skipped"
800 );
801 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
802 assert!(
803 carried["check"].is_object(),
804 "the check section is present, so the absence check below is not vacuous: {carried}"
805 );
806 assert!(
807 carried["check"].get("workspace_diagnostics").is_none(),
808 "the check section is not a second carrier: {carried}"
809 );
810
811 let empty = serialize_combined_programmatic_json(combined_output(
812 root,
813 Some(dead_code_output(root, Vec::new())),
814 None,
815 ))
816 .expect("combined JSON");
817 assert!(
818 empty.get("workspace_diagnostics").is_none(),
819 "an empty list is omitted from the combined root: {empty}"
820 );
821 }
822
823 #[test]
827 fn combined_programmatic_root_carries_workspace_diagnostics_without_a_dead_code_section() {
828 let root = Path::new("/project");
829 let carried = serialize_combined_programmatic_json(combined_output(
830 root,
831 None,
832 Some(health_output(root, vec![bun_lockb_diagnostic(root)])),
833 ))
834 .expect("combined JSON");
835 assert!(
836 carried.get("check").is_none(),
837 "this run has no check section: {carried}"
838 );
839 assert_eq!(
840 carried["workspace_diagnostics"][0]["kind"],
841 "bun-lockb-override-resolution-skipped"
842 );
843 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
844 }
845
846 #[test]
850 fn combined_programmatic_root_carries_workspace_diagnostics_from_a_duplication_only_run() {
851 let root = Path::new("/project");
852 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
853 root,
854 None,
855 None,
856 Some(duplication_output(
857 root,
858 vec![large_file_diagnostic(root, "src/generated.ts")],
859 )),
860 ))
861 .expect("combined JSON");
862 assert!(
863 carried.get("check").is_none() && carried.get("health").is_none(),
864 "only the dupes section ran: {carried}"
865 );
866 assert_eq!(root_kinds(&carried), ["skipped-large-file"]);
867 assert_eq!(
868 carried["workspace_diagnostics"][0]["path"],
869 "src/generated.ts"
870 );
871 }
872
873 #[test]
881 fn combined_programmatic_root_unions_sections_that_recorded_different_diagnostics() {
882 let root = Path::new("/project");
883 let shared = bun_lockb_diagnostic(root);
884 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
885 root,
886 Some(dead_code_output(
887 root,
888 vec![shared.clone(), large_file_diagnostic(root, "src/big.ts")],
889 )),
890 Some(health_output(root, vec![shared.clone()])),
891 Some(duplication_output(
892 root,
893 vec![shared, large_file_diagnostic(root, "src/other.ts")],
894 )),
895 ))
896 .expect("combined JSON");
897 assert_eq!(
898 root_kinds(&carried),
899 [
900 "bun-lockb-override-resolution-skipped",
901 "skipped-large-file",
902 "skipped-large-file",
903 ],
904 "the union keeps dead-code order first and drops the repeats: {}",
905 carried["workspace_diagnostics"]
906 );
907 let paths: Vec<&str> = carried["workspace_diagnostics"]
908 .as_array()
909 .expect("array")
910 .iter()
911 .map(|diagnostic| diagnostic["path"].as_str().unwrap_or_default())
912 .collect();
913 assert_eq!(paths, ["package.json", "src/big.ts", "src/other.ts"]);
914 }
915
916 #[test]
921 fn combined_programmatic_root_keeps_diagnostics_an_empty_dead_code_section_missed() {
922 let root = Path::new("/project");
923 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
924 root,
925 Some(dead_code_output(root, Vec::new())),
926 Some(health_output(
927 root,
928 vec![large_file_diagnostic(root, "src/big.test.ts")],
929 )),
930 None,
931 ))
932 .expect("combined JSON");
933 assert_eq!(root_kinds(&carried), ["skipped-large-file"]);
934 assert_eq!(
935 carried["workspace_diagnostics"][0]["path"],
936 "src/big.test.ts"
937 );
938 }
939}