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)]
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(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
182#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
183#[serde(rename_all = "kebab-case")]
184pub enum CoverageIntelligenceMatchConfidence {
185    /// Surfaces matched on path, function identity, and line.
186    PathFunctionLine,
187    /// Surfaces matched on path and line only.
188    PathLine,
189    /// Single-surface evidence; no cross-surface join was needed.
190    #[default]
191    Direct,
192}
193
194impl CoverageIntelligenceMatchConfidence {
195    /// Kebab-case wire value of the match confidence.
196    #[must_use]
197    pub const fn as_str(self) -> &'static str {
198        match self {
199            Self::PathFunctionLine => "path-function-line",
200            Self::PathLine => "path-line",
201            Self::Direct => "direct",
202        }
203    }
204}
205
206impl fmt::Display for CoverageIntelligenceMatchConfidence {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.write_str(self.as_str())
209    }
210}
211
212/// Machine-actionable next step for a coverage-intelligence finding.
213#[derive(Debug, Clone, serde::Serialize)]
214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
215pub struct CoverageIntelligenceAction {
216    /// Action identifier, normalized to `type` in JSON output.
217    #[serde(rename = "type")]
218    pub kind: String,
219    /// Human-readable action description.
220    pub description: String,
221    /// Whether fallow can apply this action automatically.
222    pub auto_fixable: bool,
223}
224
225/// Compact evidence values that led to a recommendation.
226#[derive(Debug, Clone, Default, serde::Serialize)]
227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
228pub struct CoverageIntelligenceEvidence {
229    /// Test coverage percentage (0-100), when coverage data exists.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub coverage_pct: Option<f64>,
232    /// CRAP score, when complexity and coverage both exist.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub crap: Option<f64>,
235    /// Runtime-coverage verdict label, e.g. `hot` or `cold`.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub runtime_verdict: Option<String>,
238    /// Observed runtime invocation count.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub invocations: Option<u64>,
241    /// Static usage status label, e.g. `unused`.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub static_status: Option<String>,
244    /// Static test-coverage status label, e.g. `no-test-path`.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub test_coverage: Option<String>,
247    /// True when the unit is inside the current change scope; omitted when
248    /// false.
249    #[serde(skip_serializing_if = "std::ops::Not::not")]
250    #[cfg_attr(feature = "schema", schemars(default))]
251    pub changed: bool,
252    /// Ownership-drift state label, when ownership analysis ran.
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub ownership_state: Option<String>,
255    /// Confidence tier of the cross-surface evidence join.
256    pub match_confidence: CoverageIntelligenceMatchConfidence,
257}
258
259/// One combined coverage-intelligence finding.
260#[derive(Debug, Clone, serde::Serialize)]
261#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
262pub struct CoverageIntelligenceFinding {
263    /// Stable finding ID of the form `fallow:coverage-intel:<hash>`.
264    pub id: String,
265    /// File path relative to the project root.
266    #[serde(serialize_with = "serde_path::serialize")]
267    pub path: PathBuf,
268    /// Function or export identity when known.
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub identity: Option<String>,
271    /// 1-indexed source line.
272    pub line: u32,
273    /// Verdict for this specific unit.
274    pub verdict: CoverageIntelligenceVerdict,
275    /// Ordered evidence signals behind the verdict.
276    pub signals: Vec<CoverageIntelligenceSignal>,
277    /// Recommended action family.
278    pub recommendation: CoverageIntelligenceRecommendation,
279    /// Confidence in the joined evidence.
280    pub confidence: CoverageIntelligenceConfidence,
281    /// IDs of related findings from other fallow surfaces.
282    #[serde(default, skip_serializing_if = "Vec::is_empty")]
283    #[cfg_attr(feature = "schema", schemars(default))]
284    pub related_ids: Vec<String>,
285    /// Compact evidence values behind the recommendation.
286    pub evidence: CoverageIntelligenceEvidence,
287    /// Machine-actionable follow-up actions.
288    pub actions: Vec<CoverageIntelligenceAction>,
289}
290
291/// Aggregate metadata for coverage-intelligence output.
292#[derive(Debug, Clone, Default, serde::Serialize)]
293#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
294pub struct CoverageIntelligenceSummary {
295    /// Total combined findings.
296    pub findings: usize,
297    /// Findings with the risky-change verdict.
298    pub risky_changes: usize,
299    /// Findings with the high-confidence-delete verdict.
300    pub high_confidence_deletes: usize,
301    /// Findings with the review-required verdict.
302    pub review_required: usize,
303    /// Findings with the refactor-carefully verdict.
304    pub refactor_carefully: usize,
305    /// Candidate joins dropped because the cross-surface match was ambiguous.
306    pub skipped_ambiguous_matches: usize,
307}
308
309/// Combined coverage, runtime, complexity, and change-scope verdicts.
310#[derive(Debug, Clone, serde::Serialize)]
311#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
312pub struct CoverageIntelligenceReport {
313    /// Coverage-intelligence block contract version.
314    pub schema_version: CoverageIntelligenceSchemaVersion,
315    /// Headline verdict, taken from the highest-ranked finding; `clean` when
316    /// there are none.
317    pub verdict: CoverageIntelligenceVerdict,
318    /// Aggregate finding counts.
319    pub summary: CoverageIntelligenceSummary,
320    /// Combined findings, one per matched unit.
321    pub findings: Vec<CoverageIntelligenceFinding>,
322}