Skip to main content

fallow_output/
health_coverage_intelligence.rs

1use std::fmt;
2use std::path::PathBuf;
3
4use fallow_types::serde_path;
5
6/// Coverage-intelligence JSON contract version. Scoped to the
7/// `coverage_intelligence` block and independent of the top-level fallow
8/// JSON `schema_version`.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11pub enum CoverageIntelligenceSchemaVersion {
12    /// First release of the coverage-intelligence block contract.
13    #[default]
14    #[serde(rename = "1")]
15    V1,
16}
17
18/// Headline verdict for the combined coverage-intelligence report.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[serde(rename_all = "kebab-case")]
22pub enum CoverageIntelligenceVerdict {
23    /// A changed hot path lacks test coverage.
24    RiskyChangeDetected,
25    /// Statically unused and runtime-cold; safe delete candidate.
26    HighConfidenceDelete,
27    /// Evidence conflicts; a human should look before acting.
28    ReviewRequired,
29    /// Runtime-reachable but poorly tested; behavior must be preserved.
30    RefactorCarefully,
31    /// No combined-signal findings.
32    Clean,
33    /// Evidence was insufficient to reach a verdict.
34    #[default]
35    Unknown,
36}
37
38impl CoverageIntelligenceVerdict {
39    /// Kebab-case wire value of the verdict.
40    #[must_use]
41    pub const fn as_str(self) -> &'static str {
42        match self {
43            Self::RiskyChangeDetected => "risky-change-detected",
44            Self::HighConfidenceDelete => "high-confidence-delete",
45            Self::ReviewRequired => "review-required",
46            Self::RefactorCarefully => "refactor-carefully",
47            Self::Clean => "clean",
48            Self::Unknown => "unknown",
49        }
50    }
51}
52
53impl fmt::Display for CoverageIntelligenceVerdict {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str(self.as_str())
56    }
57}
58
59/// Ordered evidence signals behind a coverage-intelligence finding.
60#[derive(
61    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
62)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(rename_all = "snake_case")]
65pub enum CoverageIntelligenceSignal {
66    /// The unit was changed in the current scope.
67    Changed,
68    /// Runtime coverage shows frequent execution.
69    HotPath,
70    /// Test coverage is below the low threshold.
71    LowTestCoverage,
72    /// CRAP score is above the risk threshold.
73    HighCrap,
74    /// Static analysis found no consumer.
75    StaticUnused,
76    /// Runtime coverage never or rarely saw it execute.
77    RuntimeCold,
78    /// No test dependency path reaches the unit.
79    NoTestPath,
80    /// Runtime coverage saw it execute.
81    RuntimeReachable,
82    /// File ownership drifted from its historic maintainers.
83    OwnershipDrift,
84    /// A test dependency path reaches the unit.
85    TestCovered,
86}
87
88impl CoverageIntelligenceSignal {
89    /// Snake-case wire value of the signal.
90    #[must_use]
91    pub const fn as_str(self) -> &'static str {
92        match self {
93            Self::Changed => "changed",
94            Self::HotPath => "hot_path",
95            Self::LowTestCoverage => "low_test_coverage",
96            Self::HighCrap => "high_crap",
97            Self::StaticUnused => "static_unused",
98            Self::RuntimeCold => "runtime_cold",
99            Self::NoTestPath => "no_test_path",
100            Self::RuntimeReachable => "runtime_reachable",
101            Self::OwnershipDrift => "ownership_drift",
102            Self::TestCovered => "test_covered",
103        }
104    }
105}
106
107impl fmt::Display for CoverageIntelligenceSignal {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.write_str(self.as_str())
110    }
111}
112
113/// Recommended action family for a combined finding.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[serde(rename_all = "kebab-case")]
117pub enum CoverageIntelligenceRecommendation {
118    /// Cover or split the risky change before merging.
119    AddTestOrSplitBeforeMerge,
120    /// Delete the unit once an owner confirms it is dead.
121    DeleteAfterConfirmingOwner,
122    /// Have a human review before changing the unit.
123    ReviewBeforeChanging,
124    /// Refactor with behavior-preserving steps only.
125    RefactorCarefullyKeepBehavior,
126}
127
128impl CoverageIntelligenceRecommendation {
129    /// Kebab-case wire value of the recommendation.
130    #[must_use]
131    pub const fn as_str(self) -> &'static str {
132        match self {
133            Self::AddTestOrSplitBeforeMerge => "add-test-or-split-before-merge",
134            Self::DeleteAfterConfirmingOwner => "delete-after-confirming-owner",
135            Self::ReviewBeforeChanging => "review-before-changing",
136            Self::RefactorCarefullyKeepBehavior => "refactor-carefully-keep-behavior",
137        }
138    }
139}
140
141impl fmt::Display for CoverageIntelligenceRecommendation {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.write_str(self.as_str())
144    }
145}
146
147/// Confidence in the joined evidence and resulting recommendation.
148#[derive(
149    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
150)]
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[serde(rename_all = "snake_case")]
153pub enum CoverageIntelligenceConfidence {
154    /// All contributing surfaces agree.
155    High,
156    /// Evidence is consistent but incomplete.
157    Medium,
158    /// Evidence is sparse or partially conflicting.
159    Low,
160}
161
162impl CoverageIntelligenceConfidence {
163    /// Snake-case wire value of the confidence level.
164    #[must_use]
165    pub const fn as_str(self) -> &'static str {
166        match self {
167            Self::High => "high",
168            Self::Medium => "medium",
169            Self::Low => "low",
170        }
171    }
172}
173
174impl fmt::Display for CoverageIntelligenceConfidence {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.write_str(self.as_str())
177    }
178}
179
180/// Confidence tier for the cross-surface evidence match.
181#[derive(
182    Debug,
183    Clone,
184    Copy,
185    Default,
186    PartialEq,
187    Eq,
188    PartialOrd,
189    Ord,
190    serde::Serialize,
191    serde::Deserialize,
192)]
193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
194#[serde(rename_all = "kebab-case")]
195pub enum CoverageIntelligenceMatchConfidence {
196    /// Surfaces matched on path, function identity, and line.
197    PathFunctionLine,
198    /// Surfaces matched on path and line only.
199    PathLine,
200    /// Single-surface evidence; no cross-surface join was needed.
201    #[default]
202    Direct,
203}
204
205impl CoverageIntelligenceMatchConfidence {
206    /// Kebab-case wire value of the match confidence.
207    #[must_use]
208    pub const fn as_str(self) -> &'static str {
209        match self {
210            Self::PathFunctionLine => "path-function-line",
211            Self::PathLine => "path-line",
212            Self::Direct => "direct",
213        }
214    }
215}
216
217impl fmt::Display for CoverageIntelligenceMatchConfidence {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.write_str(self.as_str())
220    }
221}
222
223/// Machine-actionable next step for a coverage-intelligence finding.
224#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
225#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
226pub struct CoverageIntelligenceAction {
227    /// Action identifier, normalized to `type` in JSON output.
228    #[serde(rename = "type")]
229    pub kind: String,
230    /// Human-readable action description.
231    pub description: String,
232    /// Whether fallow can apply this action automatically.
233    pub auto_fixable: bool,
234}
235
236/// Compact evidence values that led to a recommendation.
237#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
238#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
239pub struct CoverageIntelligenceEvidence {
240    /// Test coverage percentage (0-100), when coverage data exists.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub coverage_pct: Option<f64>,
243    /// CRAP score, when complexity and coverage both exist.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub crap: Option<f64>,
246    /// Runtime-coverage verdict label, e.g. `hot` or `cold`.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub runtime_verdict: Option<String>,
249    /// Observed runtime invocation count.
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub invocations: Option<u64>,
252    /// Static usage status label, e.g. `unused`.
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub static_status: Option<String>,
255    /// Static test-coverage status label, e.g. `no-test-path`.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub test_coverage: Option<String>,
258    /// True when the unit is inside the current change scope; omitted when
259    /// false.
260    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
261    #[cfg_attr(feature = "schema", schemars(default))]
262    pub changed: bool,
263    /// Ownership-drift state label, when ownership analysis ran.
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub ownership_state: Option<String>,
266    /// Confidence tier of the cross-surface evidence join.
267    pub match_confidence: CoverageIntelligenceMatchConfidence,
268}
269
270/// One combined coverage-intelligence finding.
271#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273pub struct CoverageIntelligenceFinding {
274    /// Stable finding ID of the form `fallow:coverage-intel:<hash>`.
275    pub id: String,
276    /// File path relative to the project root.
277    #[serde(serialize_with = "serde_path::serialize")]
278    pub path: PathBuf,
279    /// Function or export identity when known.
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub identity: Option<String>,
282    /// 1-indexed source line.
283    pub line: u32,
284    /// Verdict for this specific unit.
285    pub verdict: CoverageIntelligenceVerdict,
286    /// Ordered evidence signals behind the verdict.
287    pub signals: Vec<CoverageIntelligenceSignal>,
288    /// Recommended action family.
289    pub recommendation: CoverageIntelligenceRecommendation,
290    /// Confidence in the joined evidence.
291    pub confidence: CoverageIntelligenceConfidence,
292    /// IDs of related findings from other fallow surfaces.
293    #[serde(default, skip_serializing_if = "Vec::is_empty")]
294    #[cfg_attr(feature = "schema", schemars(default))]
295    pub related_ids: Vec<String>,
296    /// Compact evidence values behind the recommendation.
297    pub evidence: CoverageIntelligenceEvidence,
298    /// Machine-actionable follow-up actions.
299    pub actions: Vec<CoverageIntelligenceAction>,
300}
301
302/// Aggregate metadata for coverage-intelligence output.
303#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
304#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
305pub struct CoverageIntelligenceSummary {
306    /// Total combined findings.
307    pub findings: usize,
308    /// Findings with the risky-change verdict.
309    pub risky_changes: usize,
310    /// Findings with the high-confidence-delete verdict.
311    pub high_confidence_deletes: usize,
312    /// Findings with the review-required verdict.
313    pub review_required: usize,
314    /// Findings with the refactor-carefully verdict.
315    pub refactor_carefully: usize,
316    /// Candidate joins dropped because the cross-surface match was ambiguous.
317    pub skipped_ambiguous_matches: usize,
318}
319
320/// Combined coverage, runtime, complexity, and change-scope verdicts.
321#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
322#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
323pub struct CoverageIntelligenceReport {
324    /// Coverage-intelligence block contract version.
325    pub schema_version: CoverageIntelligenceSchemaVersion,
326    /// Headline verdict, taken from the highest-ranked finding; `clean` when
327    /// there are none.
328    pub verdict: CoverageIntelligenceVerdict,
329    /// Aggregate finding counts.
330    pub summary: CoverageIntelligenceSummary,
331    /// Combined findings, one per matched unit.
332    pub findings: Vec<CoverageIntelligenceFinding>,
333}