1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#![cfg_attr(coverage_nightly, coverage(off))]
// Data models for Five Whys root cause analysis
//
// GREEN PHASE: Minimal implementation to make tests pass
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Complete Five Whys analysis result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebugAnalysis {
pub issue: String,
pub whys: Vec<WhyIteration>,
pub root_cause: Option<String>,
pub recommendations: Vec<Recommendation>,
pub evidence_summary: EvidenceSummary,
}
impl DebugAnalysis {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Create a new instance.
pub fn new(issue: String) -> Self {
Self {
issue,
whys: Vec::new(),
root_cause: None,
recommendations: Vec::new(),
evidence_summary: EvidenceSummary::default(),
}
}
}
/// Single "Why" iteration with hypothesis and evidence
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhyIteration {
pub depth: u8,
pub question: String,
pub hypothesis: String,
pub evidence: Vec<Evidence>,
pub confidence: f64,
}
impl WhyIteration {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Create a new instance.
pub fn new(depth: u8, question: String, hypothesis: String) -> Self {
Self {
depth,
question,
hypothesis,
evidence: Vec::new(),
confidence: 0.5, // Default medium confidence
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// With confidence.
pub fn with_confidence(mut self, confidence: f64) -> Self {
self.confidence = confidence.clamp(0.0, 1.0);
self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Add evidence.
pub fn add_evidence(&mut self, evidence: Evidence) {
self.evidence.push(evidence);
}
}
/// Evidence from PMAT analysis tools
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Evidence {
pub source: EvidenceSource,
pub file: PathBuf,
pub metric: String,
pub value: serde_json::Value,
pub interpretation: String,
}
impl Evidence {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
/// Create a new instance.
pub fn new(
source: EvidenceSource,
file: PathBuf,
metric: String,
value: serde_json::Value,
interpretation: String,
) -> Self {
Self {
source,
file,
metric,
value,
interpretation,
}
}
}
/// Source of evidence (which PMAT service)
///
/// v2 weights (PMAT-510): Complexity 25%, SATD 20%, GitChurn 15%,
/// EvoScoreTrajectory 15%, CoverageDelta 15%, DeadCode 10%.
/// TDG removed (redundant with complexity+churn). ManualInspection kept for manual overrides.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EvidenceSource {
Complexity,
SATD,
DeadCode,
GitChurn,
/// Kept for backward compatibility but no longer weighted in v2
TDG,
ManualInspection,
/// CB-142 EvoScore trajectory: is the affected area improving or regressing?
EvoScoreTrajectory,
/// Coverage delta: did recent changes decrease test coverage?
CoverageDelta,
/// Source locations matching distinctive terms from the reported issue.
///
/// Every other variant here is a *repo-wide* metric that is identical no
/// matter what issue was reported. Without at least one of these, an
/// analysis has not looked at the problem it was asked about, and must not
/// present a root cause (GH #637).
IssueLocation,
}
/// Actionable recommendation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
pub priority: Priority,
pub action: String,
pub file: Option<PathBuf>,
}
impl Recommendation {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
/// Create a new instance.
pub fn new(priority: Priority, action: String, file: Option<PathBuf>) -> Self {
Self {
priority,
action,
file,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
/// High.
pub fn high(action: String, file: Option<PathBuf>) -> Self {
Self::new(Priority::High, action, file)
}
/// Medium.
pub fn medium(action: String, file: Option<PathBuf>) -> Self {
Self::new(Priority::Medium, action, file)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
/// Low.
pub fn low(action: String, file: Option<PathBuf>) -> Self {
Self::new(Priority::Low, action, file)
}
}
/// Recommendation priority
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Priority {
High,
Medium,
Low,
}
/// Summary of evidence across all Why iterations
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EvidenceSummary {
pub complexity_violations: usize,
pub satd_markers: usize,
pub tdg_score: f64,
/// Was `tdg_score` actually populated by TDG evidence?
///
/// TDG was dropped from the v2 evidence set (weight 0, redundant with
/// complexity + churn), so nothing produces TDG evidence and `tdg_score`
/// keeps its `0.0` default. Reports printed that as "TDG score 0.0/100 ❌",
/// a failing grade for something never measured — and one that contradicted
/// `pmat analyze`, which reported an average of 95.4 for the same tree
/// (GH #637).
#[serde(default)]
pub tdg_measured: bool,
pub git_churn_high: bool,
/// CB-142 EvoScore trajectory: positive = improving, negative = regressing
#[serde(default)]
pub evoscore_trajectory: f64,
/// Coverage delta: positive = coverage increased, negative = decreased
#[serde(default)]
pub coverage_delta: f64,
}
impl EvidenceSummary {
fn process_complexity_evidence(&mut self, evidence: &Evidence) {
let value = evidence
.value
.get("value")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let threshold = evidence
.value
.get("threshold")
.and_then(|t| t.as_f64())
.unwrap_or(20.0);
if value > threshold {
self.complexity_violations += 1;
}
}
fn process_satd_evidence(&mut self, evidence: &Evidence) {
self.satd_markers += evidence
.value
.get("count")
.and_then(|c| c.as_u64())
.unwrap_or(1) as usize;
}
fn process_evidence(&mut self, evidence: &Evidence) {
match evidence.source {
EvidenceSource::Complexity => self.process_complexity_evidence(evidence),
EvidenceSource::SATD => self.process_satd_evidence(evidence),
EvidenceSource::TDG => {
if let Some(score) = evidence.value.as_f64() {
self.tdg_score = score;
self.tdg_measured = true;
}
}
// Locations are rendered per-why, not in the aggregate table.
EvidenceSource::IssueLocation => {}
EvidenceSource::GitChurn => {
let commits = evidence
.value
.get("commit_count")
.and_then(|c| c.as_u64())
.unwrap_or(0);
if commits > 10 {
self.git_churn_high = true;
}
}
EvidenceSource::EvoScoreTrajectory => {
self.evoscore_trajectory = evidence
.value
.get("evoscore")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
}
EvidenceSource::CoverageDelta => {
self.coverage_delta = evidence
.value
.get("delta")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
}
_ => {}
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Aggregate the evidence behind a chain of whys, counting each distinct
/// measurement once.
///
/// Five Whys attaches the SAME repo-level measurement to every iteration —
/// each why's evidence for a 4-marker codebase says `{"count": 4}` — so
/// summing over iterations made the summary a function of `--depth` rather
/// than of the code: `satd_markers` was 4 at depth 1, 12 at depth 3 and 20
/// at depth 5 for a file containing exactly 4 markers (GH #670). The same
/// arithmetic inflated `complexity_violations` by one per iteration.
///
/// Identity of a measurement is (source, file, metric, value): two whys
/// citing genuinely different SATD findings still contribute twice.
pub fn from_whys(whys: &[WhyIteration]) -> Self {
let mut summary = Self::default();
let mut seen: std::collections::HashSet<(String, PathBuf, String, String)> =
std::collections::HashSet::new();
for why in whys {
for evidence in &why.evidence {
let key = (
format!("{:?}", evidence.source),
evidence.file.clone(),
evidence.metric.clone(),
evidence.value.to_string(),
);
if seen.insert(key) {
summary.process_evidence(evidence);
}
}
}
summary
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
include!("debug_analysis_tests.rs");
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod coverage_tests {
use super::*;
use proptest::prelude::*;
include!("debug_analysis_coverage_helpers.rs");
include!("debug_analysis_coverage_unit.rs");
include!("debug_analysis_coverage_recommend.rs");
include!("debug_analysis_coverage_props.rs");
}