Skip to main content

fallow_cli/health_types/
grouped.rs

1//! Per-group health output for `--group-by`.
2//!
3//! When health is invoked with `--group-by package` (or any other grouping
4//! mode), the orchestrator partitions the project's files by the resolver and
5//! emits one [`HealthGroup`] per bucket. Each group carries its own
6//! [`VitalSigns`] and [`HealthScore`] computed from the files in that group
7//! alone, plus the per-file output (findings, file scores, hotspots, large
8//! functions, refactoring targets) restricted to the same subset.
9
10use serde::Serialize;
11
12use crate::health_types::{
13    CoverageSourceConsistency, FileHealthScore, HealthActionsMeta, HealthFinding, HealthScore,
14    HotspotFinding, LargeFunctionEntry, RefactoringTargetFinding, VitalSigns,
15};
16
17/// A health report scoped to a single group.
18///
19/// `key` is the group label produced by the resolver (workspace package name,
20/// CODEOWNERS owner, directory, or section). `owners` is populated only for
21/// `--group-by section` (mirrors dead-code grouped output).
22///
23/// Per-group `vital_signs` and `health_score` are recomputed from the
24/// files in the group, so they answer "what is the health of workspace X" in
25/// a single invocation. `files_analyzed` and `functions_above_threshold`
26/// summarise the subset for parity with the project-level
27/// [`crate::health_types::HealthSummary`].
28#[derive(Debug, Clone, Serialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30pub struct HealthGroup {
31    /// Group identifier produced by the resolver. For 'package' grouping:
32    /// workspace package name (e.g. '@scope/app-a') or '(root)' for files
33    /// outside any workspace. For 'owner' grouping: the CODEOWNERS team. For
34    /// 'directory' grouping: the top-level directory prefix. For 'section'
35    /// grouping: the GitLab CODEOWNERS section name, or '(no section)' /
36    /// '(unowned)' for unmatched files.
37    pub key: String,
38    /// Section default owners (GitLab CODEOWNERS `[Section] @owner1 @owner2`).
39    /// Present only when grouped_by is 'section'.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub owners: Option<Vec<String>>,
42    /// Files participating in this group after workspace and ignore filters.
43    pub files_analyzed: usize,
44    /// Number of findings in this group, mirroring the project-level
45    /// `summary.functions_above_threshold` semantics post-baseline /
46    /// post-`--top` truncation. When `--top` was supplied this reflects the
47    /// rendered finding count, not the un-truncated total.
48    pub functions_above_threshold: usize,
49    /// Whether CRAP findings in this group share a single coverage-source kind
50    /// (`uniform`) or combine Istanbul / estimated / inherited sources
51    /// (`mixed`). Absent when no grouped finding carries CRAP source data.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub coverage_source_consistency: Option<CoverageSourceConsistency>,
54    /// Per-group vital signs recomputed from the files in this group. Absent
55    /// when --score-only suppressed top-level vital signs.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub vital_signs: Option<VitalSigns>,
58    /// Per-group health score recomputed from the per-group vital signs. Absent
59    /// when --score was not requested.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub health_score: Option<HealthScore>,
62    /// Findings restricted to files in this group. Each entry is the typed
63    /// [`HealthFinding`] wrapper around a
64    /// [`ComplexityViolation`](crate::health_types::ComplexityViolation)
65    /// payload.
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub findings: Vec<HealthFinding>,
68    /// File scores restricted to files in this group.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub file_scores: Vec<FileHealthScore>,
71    /// Hotspots restricted to files in this group. Each entry is the typed
72    /// [`HotspotFinding`] wrapper around a
73    /// [`HotspotEntry`](crate::health_types::HotspotEntry) payload.
74    #[serde(default, skip_serializing_if = "Vec::is_empty")]
75    pub hotspots: Vec<HotspotFinding>,
76    /// Large functions in files belonging to this group.
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub large_functions: Vec<LargeFunctionEntry>,
79    /// Refactoring targets in files belonging to this group. Each entry is
80    /// the typed [`RefactoringTargetFinding`] wrapper around a
81    /// [`RefactoringTarget`](crate::health_types::RefactoringTarget)
82    /// payload.
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub targets: Vec<RefactoringTargetFinding>,
85    /// Auditable breadcrumb recording why `suppress-line` action hints
86    /// were omitted from this group's findings. Mirrors the project-level
87    /// `HealthReport.actions_meta`; populated at construction time when the
88    /// per-group [`HealthActionContext`](crate::health_types::HealthActionContext)
89    /// suppresses inline hints.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub actions_meta: Option<HealthActionsMeta>,
92}
93
94/// Wrapper carrying the resolver mode label alongside the partitioned groups.
95///
96/// Stored on `crate::health::HealthResult` when `--group-by` is active and
97/// consumed by formatters that either render grouped data directly or annotate
98/// per-finding machine output with the group key.
99#[derive(Debug, Clone)]
100pub struct HealthGrouping {
101    /// Resolver mode label (`"package"`, `"owner"`, `"directory"`, `"section"`).
102    pub mode: &'static str,
103    /// Groups in the same order the resolver produced them.
104    pub groups: Vec<HealthGroup>,
105}