Skip to main content

fallow_api/
json_output.rs

1//! Shared JSON output assembly for CLI and programmatic consumers.
2
3use std::path::Path;
4use std::time::Duration;
5
6use fallow_output::{
7    CHECK_SCHEMA_VERSION, CheckEnvelope, CheckGroupedEntry, CheckGroupedOutput, CheckOutput,
8    CheckOutputInput, DUPES_SCHEMA_VERSION, DupesOutput, DupesOutputInput, GroupByMode,
9    apply_config_fixable_to_duplicate_exports, build_check_output, build_dupes_output,
10    harmonize_multi_kind_suppress_line_actions as harmonize_typed_suppress_line_actions,
11    strip_root_prefix,
12};
13use fallow_types::duplicates::DuplicationReport;
14use fallow_types::envelope::{
15    BaselineDeltas, BaselineMatch, ElapsedMs, Meta, RegressionResult, SchemaVersion, ToolVersion,
16};
17use fallow_types::output::NextStep;
18use fallow_types::results::AnalysisResults;
19use fallow_types::workspace::WorkspaceDiagnostic;
20
21use crate::{DupesReportPayload, DuplicationGroup, DuplicationGrouping, ResultGroup};
22
23/// Inputs for `fallow dead-code --format json` and
24/// `fallow architecture --format json` output assembly.
25pub struct CheckJsonOutputInput<'a> {
26    /// The command family member: sets the root `kind` and `schema_version`.
27    pub envelope: CheckEnvelope,
28    /// Typed dead-code results to serialize.
29    pub results: &'a AnalysisResults,
30    /// Project root; its prefix is stripped from every path in the output.
31    pub root: &'a Path,
32    /// Analysis wall time, emitted as `elapsed_ms`.
33    pub elapsed: Duration,
34    /// Whether duplicate-export findings can be auto-fixed through config;
35    /// propagated onto their fix actions.
36    pub config_fixable: bool,
37    /// Optional explain metadata block for the envelope.
38    pub meta: Option<Meta>,
39    /// Caller-computed baseline and regression sections.
40    pub extras: CheckJsonExtraOutputs,
41    /// Non-fatal per-file diagnostics collected during the workspace walk.
42    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
43    /// Suggested follow-up commands for the consumer.
44    pub next_steps: Vec<NextStep>,
45    /// Analysis run id stamped into telemetry metadata when present.
46    pub telemetry_analysis_run_id: Option<&'a str>,
47}
48
49/// Inputs for the dead-code JSON payload without a root envelope.
50pub struct CheckJsonPayloadInput<'a> {
51    /// Typed dead-code results to serialize.
52    pub results: &'a AnalysisResults,
53    /// Project root; its prefix is stripped from every path in the output.
54    pub root: &'a Path,
55    /// Analysis wall time, emitted as `elapsed_ms`.
56    pub elapsed: Duration,
57    /// Whether duplicate-export findings can be auto-fixed through config;
58    /// propagated onto their fix actions.
59    pub config_fixable: bool,
60    /// Caller-computed baseline and regression sections.
61    pub extras: CheckJsonExtraOutputs,
62    /// Non-fatal per-file diagnostics collected during the workspace walk.
63    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
64}
65
66/// Optional root sections for dead-code JSON envelopes.
67///
68/// These fields are part of the output contract, but they are computed by
69/// caller-specific workflows such as baseline and regression gates.
70#[derive(Debug, Clone, Default)]
71pub struct CheckJsonExtraOutputs {
72    /// Applied refs for exact workspace package roots.
73    pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
74    /// Per-category issue count changes against the matched baseline.
75    pub baseline_deltas: Option<BaselineDeltas>,
76    /// Which baseline snapshot the run was compared against.
77    pub baseline: Option<BaselineMatch>,
78    /// This run's view of that baseline: counts, advisory verdict and the
79    /// `--fail-on-stale-baseline` verdict.
80    pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
81    /// Outcome of the regression gate against the baseline.
82    pub regression: Option<RegressionResult>,
83    /// Every gate this run evaluated. The programmatic route runs no CLI-layer
84    /// gate, so a caller that computes none leaves this `None` and the envelope
85    /// key stays absent. An empty set is never emitted: it would assert that
86    /// gates were evaluated and none tripped, which is a different claim.
87    pub gate_outcomes: Option<fallow_output::GateOutcomes>,
88    /// Every narrowing or shaping request this run received, absent when it
89    /// was asked for nothing. The programmatic route resolves no CLI flag and
90    /// leaves this `None`; an entry whose `status` is not `applied` means the
91    /// report is wider than what was asked for.
92    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
93    /// The answer to a finding-id query, present only when the run received
94    /// one or more finding ids. See [`fallow_output::FindingIdQuery`].
95    pub finding_id_query: Option<fallow_output::FindingIdQuery>,
96}
97
98struct CheckJsonEnvelopeInput<'a> {
99    schema_version: u32,
100    results: &'a AnalysisResults,
101    elapsed: Duration,
102    config_fixable: bool,
103    meta: Option<Meta>,
104    extras: CheckJsonExtraOutputs,
105    workspace_diagnostics: Vec<WorkspaceDiagnostic>,
106    next_steps: Vec<NextStep>,
107}
108
109/// Inputs for grouped dead-code and architecture JSON output assembly.
110pub struct GroupedCheckJsonOutputInput<'a> {
111    /// The command family member: sets the root `kind` and `schema_version`.
112    pub envelope: CheckEnvelope,
113    /// Applied refs for exact workspace package roots.
114    pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
115    /// This run's view of the loaded baseline, for baseline runs.
116    pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
117    /// Every gate this run evaluated. The programmatic route runs no CLI-layer
118    /// gate, so a caller that computes none leaves this `None` and the envelope
119    /// key stays absent. An empty set is never emitted: it would assert that
120    /// gates were evaluated and none tripped, which is a different claim.
121    pub gate_outcomes: Option<fallow_output::GateOutcomes>,
122    /// Every narrowing or shaping request this run received, absent when it
123    /// was asked for nothing. The programmatic route resolves no CLI flag and
124    /// leaves this `None`; an entry whose `status` is not `applied` means the
125    /// report is wider than what was asked for.
126    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
127    /// The answer to a finding-id query, present only when the run received
128    /// one or more finding ids. See [`fallow_output::FindingIdQuery`].
129    pub finding_id_query: Option<fallow_output::FindingIdQuery>,
130
131    /// Results already partitioned into groups, in output order.
132    pub groups: &'a [ResultGroup],
133    /// Ungrouped results, used for the envelope's `total_issues` count.
134    pub original: &'a AnalysisResults,
135    /// Project root; its prefix is stripped from every path in the output.
136    pub root: &'a Path,
137    /// Analysis wall time, emitted as `elapsed_ms`.
138    pub elapsed: Duration,
139    /// Grouping axis recorded as `grouped_by` in the envelope.
140    pub grouped_by: GroupByMode,
141    /// Whether duplicate-export findings can be auto-fixed through config;
142    /// propagated onto their fix actions per group.
143    pub config_fixable: bool,
144    /// Optional explain metadata block for the envelope.
145    pub meta: Option<Meta>,
146    /// Non-fatal per-file diagnostics collected during the workspace walk.
147    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
148    /// Suggested follow-up commands for the consumer.
149    pub next_steps: Vec<NextStep>,
150    /// Analysis run id stamped into telemetry metadata when present.
151    pub telemetry_analysis_run_id: Option<&'a str>,
152}
153
154/// Inputs for `fallow dupes --format json` output assembly.
155pub struct DuplicationJsonOutputInput<'a> {
156    /// Applied refs for exact workspace package roots.
157    pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
158    /// This run's view of the loaded duplication baseline, for baseline runs.
159    pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
160    /// Every gate this run evaluated. The programmatic route runs no CLI-layer
161    /// gate, so a caller that computes none leaves this `None` and the envelope
162    /// key stays absent. An empty set is never emitted: it would assert that
163    /// gates were evaluated and none tripped, which is a different claim.
164    pub gate_outcomes: Option<fallow_output::GateOutcomes>,
165    /// Every narrowing or shaping request this run received, absent when it
166    /// was asked for nothing. The programmatic route resolves no CLI flag and
167    /// leaves this `None`; an entry whose `status` is not `applied` means the
168    /// report is wider than what was asked for.
169    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
170
171    /// Typed duplication report to serialize.
172    pub report: &'a DuplicationReport,
173    /// Project root; its prefix is stripped from every path in the output.
174    pub root: &'a Path,
175    /// Analysis wall time, emitted as `elapsed_ms`.
176    pub elapsed: Duration,
177    /// Whether each clone instance carries its verbatim source text.
178    pub include_fragments: bool,
179    /// Optional explain metadata block for the envelope.
180    pub meta: Option<Meta>,
181    /// Non-fatal per-file diagnostics collected during the workspace walk.
182    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
183    /// Suggested follow-up commands for the consumer.
184    pub next_steps: Vec<NextStep>,
185    /// Analysis run id stamped into telemetry metadata when present.
186    pub telemetry_analysis_run_id: Option<&'a str>,
187}
188
189/// Inputs for grouped duplication JSON output assembly.
190pub struct GroupedDuplicationJsonOutputInput<'a> {
191    /// Applied refs for exact workspace package roots.
192    pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
193    /// This run's view of the loaded duplication baseline, for baseline runs.
194    pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
195    /// Every gate this run evaluated. The programmatic route runs no CLI-layer
196    /// gate, so a caller that computes none leaves this `None` and the envelope
197    /// key stays absent. An empty set is never emitted: it would assert that
198    /// gates were evaluated and none tripped, which is a different claim.
199    pub gate_outcomes: Option<fallow_output::GateOutcomes>,
200    /// Every narrowing or shaping request this run received, absent when it
201    /// was asked for nothing. The programmatic route resolves no CLI flag and
202    /// leaves this `None`; an entry whose `status` is not `applied` means the
203    /// report is wider than what was asked for.
204    pub request_outcomes: Option<fallow_output::RequestOutcomes>,
205
206    /// Typed duplication report to serialize.
207    pub report: &'a DuplicationReport,
208    /// Precomputed grouping whose groups replace the flat `groups` array.
209    pub grouping: &'a DuplicationGrouping,
210    /// Project root; its prefix is stripped from every path in the output.
211    pub root: &'a Path,
212    /// Analysis wall time, emitted as `elapsed_ms`.
213    pub elapsed: Duration,
214    /// Whether each clone instance carries its verbatim source text.
215    pub include_fragments: bool,
216    /// Optional explain metadata block for the envelope.
217    pub meta: Option<Meta>,
218    /// Non-fatal per-file diagnostics collected during the workspace walk.
219    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
220    /// Suggested follow-up commands for the consumer.
221    pub next_steps: Vec<NextStep>,
222    /// Analysis run id stamped into telemetry metadata when present.
223    pub telemetry_analysis_run_id: Option<&'a str>,
224}
225
226/// Build and serialize dead-code JSON through the API-owned output boundary.
227///
228/// # Errors
229///
230/// Returns a serde error when the typed envelope cannot be converted to JSON.
231pub fn serialize_check_json(
232    input: CheckJsonOutputInput<'_>,
233) -> Result<serde_json::Value, serde_json::Error> {
234    let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
235        schema_version: input.envelope.schema_version(),
236        results: input.results,
237        elapsed: input.elapsed,
238        config_fixable: input.config_fixable,
239        meta: input.meta,
240        extras: input.extras,
241        workspace_diagnostics: input.workspace_diagnostics,
242        next_steps: input.next_steps,
243    });
244    let run_id = input.telemetry_analysis_run_id;
245    let mut output = match input.envelope {
246        CheckEnvelope::DeadCode => fallow_output::serialize_check_json_output(envelope, run_id)?,
247        CheckEnvelope::Architecture => {
248            fallow_output::serialize_architecture_json_output(envelope, run_id)?
249        }
250    };
251    strip_json_root_prefix(&mut output, input.root);
252    Ok(output)
253}
254
255/// Build a dead-code JSON payload without adding a root envelope.
256///
257/// # Errors
258///
259/// Returns a serde error when the typed envelope cannot be converted to JSON.
260pub fn serialize_check_json_payload(
261    input: CheckJsonPayloadInput<'_>,
262) -> Result<serde_json::Value, serde_json::Error> {
263    let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
264        schema_version: CHECK_SCHEMA_VERSION,
265        results: input.results,
266        elapsed: input.elapsed,
267        config_fixable: input.config_fixable,
268        meta: None,
269        extras: input.extras,
270        workspace_diagnostics: input.workspace_diagnostics,
271        next_steps: Vec::new(),
272    });
273    let mut output = serde_json::to_value(envelope)?;
274    strip_json_root_prefix(&mut output, input.root);
275    Ok(output)
276}
277
278/// Build and serialize grouped dead-code JSON through the API output boundary.
279///
280/// # Errors
281///
282/// Returns a serde error when the typed envelope cannot be converted to JSON.
283pub fn serialize_grouped_check_json(
284    input: GroupedCheckJsonOutputInput<'_>,
285) -> Result<serde_json::Value, serde_json::Error> {
286    let entries = input
287        .groups
288        .iter()
289        .map(|group| {
290            let mut results = group.results.clone();
291            apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
292            harmonize_typed_suppress_line_actions(&mut results);
293            CheckGroupedEntry {
294                key: group.key.clone(),
295                owners: group.owners.clone(),
296                total_issues: results.total_issues(),
297                results,
298            }
299        })
300        .collect();
301
302    let envelope = CheckGroupedOutput {
303        package_baselines: input.package_baselines,
304        request_outcomes: input.request_outcomes,
305        schema_version: SchemaVersion(input.envelope.schema_version()),
306        version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
307        elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
308        grouped_by: input.grouped_by,
309        total_issues: input.original.total_issues(),
310        groups: entries,
311        unused_load_data_keys_global_abstain: input.original.unused_load_data_keys_global_abstain,
312        cascade_hidden: input.original.cascade_hidden,
313        baseline_staleness: input.baseline_staleness,
314        finding_id_query: input.finding_id_query,
315        gate_outcomes: input.gate_outcomes,
316        meta: input.meta,
317        workspace_diagnostics: input.workspace_diagnostics,
318        next_steps: input.next_steps,
319    };
320
321    let run_id = input.telemetry_analysis_run_id;
322    let mut output = match input.envelope {
323        CheckEnvelope::DeadCode => {
324            fallow_output::serialize_check_grouped_json_output(envelope, run_id)?
325        }
326        CheckEnvelope::Architecture => {
327            fallow_output::serialize_architecture_grouped_json_output(envelope, run_id)?
328        }
329    };
330    strip_json_root_prefix(&mut output, input.root);
331    Ok(output)
332}
333
334/// Build and serialize duplication JSON through the API-owned output boundary.
335///
336/// # Errors
337///
338/// Returns a serde error when the typed envelope cannot be converted to JSON.
339pub fn serialize_duplication_json(
340    input: DuplicationJsonOutputInput<'_>,
341) -> Result<serde_json::Value, serde_json::Error> {
342    let payload =
343        DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
344    let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
345        build_dupes_output(DupesOutputInput {
346            gate_outcomes: input.gate_outcomes,
347            request_outcomes: input.request_outcomes,
348            schema_version: DUPES_SCHEMA_VERSION,
349            version: env!("CARGO_PKG_VERSION").to_string(),
350            elapsed: input.elapsed,
351            report: payload,
352            clone_groups_shown: input.report.clone_groups_shown(),
353            clone_groups_omitted: input.report.clone_groups_omitted(),
354            clone_families_shown: input.report.clone_families_shown(),
355            clone_families_omitted: input.report.clone_families_omitted(),
356            grouped_by: None,
357            total_issues: None,
358            groups: None,
359            baseline_staleness: input.baseline_staleness,
360            meta: input.meta,
361            workspace_diagnostics: input.workspace_diagnostics,
362            next_steps: input.next_steps,
363        });
364    envelope.package_baselines = input.package_baselines;
365    let mut output =
366        fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
367    let root_prefix = format!("{}/", input.root.display());
368    strip_root_prefix(&mut output, &root_prefix);
369    Ok(output)
370}
371
372/// Build and serialize grouped duplication JSON through the API output boundary.
373///
374/// # Errors
375///
376/// Returns a serde error when the typed envelope cannot be converted to JSON.
377pub fn serialize_grouped_duplication_json(
378    input: GroupedDuplicationJsonOutputInput<'_>,
379) -> Result<serde_json::Value, serde_json::Error> {
380    let root_prefix = format!("{}/", input.root.display());
381    let payload =
382        DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
383    let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
384        build_dupes_output(DupesOutputInput {
385            gate_outcomes: input.gate_outcomes,
386            request_outcomes: input.request_outcomes,
387            schema_version: DUPES_SCHEMA_VERSION,
388            version: env!("CARGO_PKG_VERSION").to_string(),
389            elapsed: input.elapsed,
390            report: payload,
391            clone_groups_shown: input.report.clone_groups_shown(),
392            clone_groups_omitted: input.report.clone_groups_omitted(),
393            clone_families_shown: input.report.clone_families_shown(),
394            clone_families_omitted: input.report.clone_families_omitted(),
395            grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
396            total_issues: Some(input.report.clone_groups.len()),
397            groups: None,
398            baseline_staleness: input.baseline_staleness,
399            meta: input.meta,
400            workspace_diagnostics: input.workspace_diagnostics,
401            next_steps: input.next_steps,
402        });
403    envelope.package_baselines = input.package_baselines;
404    let mut output =
405        fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
406    strip_root_prefix(&mut output, &root_prefix);
407
408    let group_values = input
409        .grouping
410        .groups
411        .iter()
412        .map(|group| {
413            let mut value = if input.include_fragments {
414                serde_json::to_value(group)?
415            } else {
416                let mut stripped = group.clone();
417                stripped.strip_fragments();
418                serde_json::to_value(&stripped)?
419            };
420            strip_root_prefix(&mut value, &root_prefix);
421            Ok(value)
422        })
423        .collect::<Result<Vec<_>, serde_json::Error>>()?;
424
425    if let serde_json::Value::Object(ref mut map) = output {
426        map.insert("groups".to_string(), serde_json::Value::Array(group_values));
427    }
428
429    Ok(output)
430}
431
432fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
433    let mut output = build_check_output(CheckOutputInput {
434        schema_version: input.schema_version,
435        version: env!("CARGO_PKG_VERSION").to_string(),
436        elapsed: input.elapsed,
437        results: input.results.clone(),
438        config_fixable: input.config_fixable,
439        meta: input.meta,
440        workspace_diagnostics: input.workspace_diagnostics,
441        next_steps: input.next_steps,
442    });
443    output.baseline_deltas = input.extras.baseline_deltas;
444    output.baseline = input.extras.baseline;
445    output.baseline_staleness = input.extras.baseline_staleness;
446    output.regression = input.extras.regression;
447    output.gate_outcomes = input.extras.gate_outcomes;
448    output.request_outcomes = input.extras.request_outcomes;
449    output.package_baselines = input.extras.package_baselines;
450    output.finding_id_query = input.extras.finding_id_query;
451    output
452}
453
454fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
455    let root_prefix = format!("{}/", root.display());
456    strip_root_prefix(output, &root_prefix);
457}
458
459fn group_by_mode_from_label(label: &str) -> GroupByMode {
460    match label {
461        "directory" => GroupByMode::Directory,
462        "package" => GroupByMode::Package,
463        "section" => GroupByMode::Section,
464        _ => GroupByMode::Owner,
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use fallow_types::workspace::WorkspaceDiagnosticKind;
472
473    #[test]
474    fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
475        let root = Path::new("/project");
476        let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
477            envelope: CheckEnvelope::DeadCode,
478            package_baselines: Vec::new(),
479            gate_outcomes: None,
480            request_outcomes: None,
481            finding_id_query: None,
482            baseline_staleness: None,
483            groups: &[],
484            original: &AnalysisResults::default(),
485            root,
486            elapsed: Duration::ZERO,
487            grouped_by: GroupByMode::Directory,
488            config_fixable: false,
489            meta: None,
490            workspace_diagnostics: vec![WorkspaceDiagnostic::new(
491                root,
492                root.join("src/unreadable.ts"),
493                WorkspaceDiagnosticKind::SourceReadFailure {
494                    error: "permission denied".to_string(),
495                },
496            )],
497            next_steps: Vec::new(),
498            telemetry_analysis_run_id: None,
499        })
500        .expect("grouped check JSON serializes");
501
502        assert_eq!(
503            output["workspace_diagnostics"][0]["path"],
504            "src/unreadable.ts"
505        );
506        assert_eq!(
507            output["workspace_diagnostics"][0]["kind"],
508            "source-read-failure"
509        );
510        assert_eq!(output["kind"], "dead-code-grouped");
511        assert_eq!(output["schema_version"], CHECK_SCHEMA_VERSION);
512    }
513
514    #[test]
515    fn architecture_envelope_sets_kind_and_schema_version() {
516        let root = Path::new("/project");
517        let results = AnalysisResults::default();
518        let flat = serialize_check_json(CheckJsonOutputInput {
519            envelope: CheckEnvelope::Architecture,
520            results: &results,
521            root,
522            elapsed: Duration::ZERO,
523            config_fixable: false,
524            meta: None,
525            extras: CheckJsonExtraOutputs::default(),
526            workspace_diagnostics: Vec::new(),
527            next_steps: Vec::new(),
528            telemetry_analysis_run_id: None,
529        })
530        .expect("architecture JSON serializes");
531        assert_eq!(flat["kind"], "architecture");
532        assert_eq!(
533            flat["schema_version"],
534            fallow_output::ARCHITECTURE_SCHEMA_VERSION
535        );
536
537        let grouped = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
538            envelope: CheckEnvelope::Architecture,
539            package_baselines: Vec::new(),
540            gate_outcomes: None,
541            request_outcomes: None,
542            finding_id_query: None,
543            baseline_staleness: None,
544            groups: &[],
545            original: &results,
546            root,
547            elapsed: Duration::ZERO,
548            grouped_by: GroupByMode::Directory,
549            config_fixable: false,
550            meta: None,
551            workspace_diagnostics: Vec::new(),
552            next_steps: Vec::new(),
553            telemetry_analysis_run_id: None,
554        })
555        .expect("grouped architecture JSON serializes");
556        assert_eq!(grouped["kind"], "architecture-grouped");
557        assert_eq!(
558            grouped["schema_version"],
559            fallow_output::ARCHITECTURE_SCHEMA_VERSION
560        );
561
562        let payload = serialize_check_json_payload(CheckJsonPayloadInput {
563            results: &results,
564            root,
565            elapsed: Duration::ZERO,
566            config_fixable: false,
567            extras: CheckJsonExtraOutputs::default(),
568            workspace_diagnostics: Vec::new(),
569        })
570        .expect("embedded payload serializes");
571        assert_eq!(payload["schema_version"], CHECK_SCHEMA_VERSION);
572        assert!(payload.get("kind").is_none());
573    }
574}