Skip to main content

garbage_code_hunter/
finding.rs

1//! StyleFinding — the structured representation of a code style observation.
2//!
3//! Evolves from the raw `CodeIssue` to include confidence, evidence,
4//! category context, and actionable suggestions. This is the standard
5//! data model for all outputs (terminal, JSON, Markdown, CI, etc.).
6
7use crate::analyzer::{CodeIssue, Severity};
8use crate::signals::{classify_rule, StyleProfile, StyleSignal};
9use std::collections::HashMap;
10use std::hash::{Hash, Hasher};
11use std::path::PathBuf;
12
13// ── Identity ─────────────────────────────────────────────────────
14
15/// Unique identifier for a finding.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct FindingId(String);
18
19impl FindingId {
20    pub fn new(seed: &str) -> Self {
21        let mut hasher = std::hash::DefaultHasher::new();
22        seed.hash(&mut hasher);
23        Self(format!("{:016x}", hasher.finish())[..12].to_string())
24    }
25
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31// ── Location ─────────────────────────────────────────────────────
32
33/// Location in source code with optional span and symbol name.
34#[derive(Debug, Clone)]
35pub struct CodeLocation {
36    pub file_path: PathBuf,
37    pub line: usize,
38    pub column: usize,
39    pub span: Option<TextSpan>,
40    pub symbol_name: Option<String>,
41}
42
43/// A contiguous range of text in source code.
44#[derive(Debug, Clone)]
45pub struct TextSpan {
46    pub start_line: usize,
47    pub start_column: usize,
48    pub end_line: usize,
49    pub end_column: usize,
50}
51
52// ── Rule Metadata ────────────────────────────────────────────────
53
54/// Meta information about the rule that triggered the finding.
55#[derive(Debug, Clone)]
56pub struct RuleMeta {
57    pub name: String,
58    pub category: StyleCategory,
59    pub intent: RuleIntent,
60}
61
62/// Code style categories — what aspect of the code is being evaluated.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum StyleCategory {
65    Naming,
66    Complexity,
67    Duplication,
68    Comments,
69    DebuggingLeftovers,
70    Structure,
71    Consistency,
72    DependencyStyle,
73}
74
75impl StyleCategory {
76    pub fn display_name(&self) -> &'static str {
77        match self {
78            StyleCategory::Naming => "Naming",
79            StyleCategory::Complexity => "Complexity",
80            StyleCategory::Duplication => "Duplication",
81            StyleCategory::Comments => "Comments",
82            StyleCategory::DebuggingLeftovers => "Debugging Leftovers",
83            StyleCategory::Structure => "Structure",
84            StyleCategory::Consistency => "Consistency",
85            StyleCategory::DependencyStyle => "Dependency Style",
86        }
87    }
88}
89
90/// Intent behind a rule — why this rule exists.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum RuleIntent {
93    Readability,
94    Maintainability,
95    TeamConvention,
96    NoiseReduction,
97    CognitiveLoad,
98}
99
100impl RuleIntent {
101    pub fn display_name(&self) -> &'static str {
102        match self {
103            RuleIntent::Readability => "Readability",
104            RuleIntent::Maintainability => "Maintainability",
105            RuleIntent::TeamConvention => "Team Convention",
106            RuleIntent::NoiseReduction => "Noise Reduction",
107            RuleIntent::CognitiveLoad => "Cognitive Load",
108        }
109    }
110}
111
112// ── Confidence ──────────────────────────────────────────────────
113
114/// How confident we are that this is a real issue vs. a false positive.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum Confidence {
117    Low,
118    Medium,
119    High,
120}
121
122impl Confidence {
123    pub fn display_name(&self) -> &'static str {
124        match self {
125            Confidence::Low => "Low",
126            Confidence::Medium => "Medium",
127            Confidence::High => "High",
128        }
129    }
130
131    pub fn score(&self) -> f64 {
132        match self {
133            Confidence::Low => 0.3,
134            Confidence::Medium => 0.6,
135            Confidence::High => 1.0,
136        }
137    }
138}
139
140// ── Evidence ────────────────────────────────────────────────────
141
142/// Evidence supporting the finding.
143#[derive(Debug, Clone)]
144pub struct Evidence {
145    pub snippet: Option<String>,
146    pub metric: Option<EvidenceMetric>,
147    pub nearby_context: Vec<String>,
148}
149
150/// A numeric metric with threshold comparison.
151#[derive(Debug, Clone)]
152pub struct EvidenceMetric {
153    pub name: String,
154    pub value: f64,
155    pub threshold: f64,
156    pub unit: String,
157}
158
159// ── Suggestion ──────────────────────────────────────────────────
160
161/// A suggested fix or improvement for the finding.
162#[derive(Debug, Clone)]
163pub struct StyleSuggestion {
164    pub title: String,
165    pub explanation: String,
166    pub quick_fix_hint: Option<String>,
167    pub safer_alternative: Option<String>,
168}
169
170// ── StyleFinding ────────────────────────────────────────────────
171
172/// The core finding model — a structured observation about code style.
173///
174/// Each finding represents one detected issue with full metadata
175/// for presentation, filtering, and educational feedback.
176#[derive(Debug, Clone)]
177pub struct StyleFinding {
178    pub id: FindingId,
179    pub location: CodeLocation,
180    pub rule: RuleMeta,
181    pub signal: StyleSignal,
182    pub severity: Severity,
183    pub confidence: Confidence,
184    pub evidence: Evidence,
185    pub suggestion: Option<StyleSuggestion>,
186}
187
188impl StyleFinding {
189    /// Create a finding from a signal-level detection (no single-line location).
190    pub fn for_signal(signal: StyleSignal, violation_count: usize, file_path: PathBuf) -> Self {
191        let id = FindingId::new(&format!(
192            "signal:{:?}:{violation_count}:{}",
193            signal,
194            file_path.display()
195        ));
196        let severity = if violation_count > 10 {
197            Severity::Nuclear
198        } else if violation_count > 3 {
199            Severity::Spicy
200        } else {
201            Severity::Mild
202        };
203        StyleFinding {
204            id,
205            location: CodeLocation {
206                file_path,
207                line: 0,
208                column: 0,
209                span: None,
210                symbol_name: None,
211            },
212            rule: RuleMeta {
213                name: signal.display_name().to_string(),
214                category: StyleCategory::Consistency,
215                intent: RuleIntent::Maintainability,
216            },
217            signal,
218            severity,
219            confidence: Confidence::High,
220            evidence: Evidence {
221                snippet: Some(format!(
222                    "{violation_count} {} violations",
223                    signal.display_name()
224                )),
225                metric: Some(EvidenceMetric {
226                    name: "violations".to_string(),
227                    value: violation_count as f64,
228                    threshold: 0.0,
229                    unit: "count".to_string(),
230                }),
231                nearby_context: Vec::new(),
232            },
233            suggestion: None,
234        }
235    }
236
237    /// Convert back to a `CodeIssue` for downstream consumers that
238    /// still depend on the legacy issue model.
239    pub fn to_code_issue(&self) -> CodeIssue {
240        CodeIssue {
241            file_path: self.location.file_path.clone(),
242            line: self.location.line,
243            column: self.location.column,
244            rule_name: self.rule.name.clone(),
245            message: self
246                .evidence
247                .snippet
248                .clone()
249                .unwrap_or_else(|| format!("{:?}", self.signal)),
250            severity: self.severity.clone(),
251        }
252    }
253}
254
255impl From<&CodeIssue> for StyleFinding {
256    fn from(issue: &CodeIssue) -> Self {
257        let id = FindingId::new(&format!(
258            "{}:{}:{}:{}",
259            issue.file_path.display(),
260            issue.line,
261            issue.rule_name,
262            issue.message,
263        ));
264        let signal = classify_rule(&issue.rule_name);
265        let location = CodeLocation {
266            file_path: issue.file_path.clone(),
267            line: issue.line,
268            column: issue.column,
269            span: None,
270            symbol_name: None,
271        };
272        let rule = RuleMeta {
273            name: issue.rule_name.clone(),
274            category: rule_to_category(&issue.rule_name),
275            intent: rule_to_intent(&issue.rule_name),
276        };
277        let confidence = rule_to_confidence(&issue.rule_name);
278        let evidence = Evidence {
279            snippet: Some(issue.message.clone()),
280            metric: None,
281            nearby_context: Vec::new(),
282        };
283
284        StyleFinding {
285            id,
286            location,
287            rule,
288            signal,
289            severity: issue.severity.clone(),
290            confidence,
291            evidence,
292            suggestion: None,
293        }
294    }
295}
296
297// ── Category / Intent / Confidence mapping ──────────────────────
298
299fn rule_to_category(rule_name: &str) -> StyleCategory {
300    match rule_name {
301        n if n.contains("naming")
302            || n.contains("letter")
303            || n.contains("hungarian")
304            || n.contains("abbreviation")
305            || n.contains("name")
306            || n.contains("predicate") =>
307        {
308            StyleCategory::Naming
309        }
310        n if n.contains("nest")
311            || n.contains("complex")
312            || n.contains("function_length")
313            || n.contains("long-function")
314            || n.contains("god-function")
315            || n.contains("too-many-params")
316            || n.contains("module-complexity")
317            || n.contains("trait-complexity") =>
318        {
319            StyleCategory::Complexity
320        }
321        n if n.contains("duplicat") => StyleCategory::Duplication,
322        n if n.contains("todo")
323            || n.contains("fixme")
324            || n.contains("commented")
325            || n.contains("dead-code") =>
326        {
327            StyleCategory::Comments
328        }
329        n if n.contains("println")
330            || n.contains("unwrap")
331            || n.contains("panic")
332            || n.contains("except")
333            || n.contains("rescue") =>
334        {
335            StyleCategory::DebuggingLeftovers
336        }
337        n if n.contains("file-too-long")
338            || n.contains("module-nesting")
339            || n.contains("import") =>
340        {
341            StyleCategory::Structure
342        }
343        n if n.contains("generic") || n.contains("magic") || n.contains("constant-name") => {
344            StyleCategory::Consistency
345        }
346        _ => StyleCategory::Consistency,
347    }
348}
349
350fn rule_to_intent(rule_name: &str) -> RuleIntent {
351    match rule_name {
352        n if n.contains("naming")
353            || n.contains("letter")
354            || n.contains("name")
355            || n.contains("hungarian")
356            || n.contains("abbreviation") =>
357        {
358            RuleIntent::Readability
359        }
360        n if n.contains("nest")
361            || n.contains("complex")
362            || n.contains("long-function")
363            || n.contains("god-function")
364            || n.contains("too-many-params") =>
365        {
366            RuleIntent::CognitiveLoad
367        }
368        n if n.contains("duplicat") => RuleIntent::Maintainability,
369        n if n.contains("todo")
370            || n.contains("fixme")
371            || n.contains("commented")
372            || n.contains("dead-code") =>
373        {
374            RuleIntent::NoiseReduction
375        }
376        n if n.contains("println")
377            || n.contains("unwrap")
378            || n.contains("panic")
379            || n.contains("except")
380            || n.contains("rescue") =>
381        {
382            RuleIntent::NoiseReduction
383        }
384        n if n.contains("magic") || n.contains("constant-name") || n.contains("generic") => {
385            RuleIntent::Maintainability
386        }
387        _ => RuleIntent::Readability,
388    }
389}
390
391fn rule_to_confidence(rule_name: &str) -> Confidence {
392    // High-confidence: objectively measurable
393    if matches!(
394        rule_name,
395        "magic-number"
396            | "code-duplication"
397            | "cross-file-duplication"
398            | "unwrap-abuse"
399            | "panic-abuse"
400            | "empty-catch"
401            | "bare-except"
402            | "bare-rescue"
403            | "println-debugging"
404            | "dead-code"
405            | "commented-code"
406            | "file-too-long"
407    ) {
408        return Confidence::High;
409    }
410
411    // Medium-confidence: measurable but context-dependent
412    if matches!(
413        rule_name,
414        "deep-nesting"
415            | "cyclomatic-complexity"
416            | "complex-closure"
417            | "long-function"
418            | "god-function"
419            | "too-many-params"
420            | "module-complexity"
421            | "trait-complexity"
422            | "todo-comment"
423            | "todo-fixme"
424            | "todo-bug"
425            | "todo-hack"
426    ) {
427        return Confidence::Medium;
428    }
429
430    // Low-confidence: subjective / style preference
431    Confidence::Low
432}
433
434// ── Universal Style IR ──────────────────────────────────────────
435//
436// These functions build StyleProfile directly from StyleFinding[*].signal,
437// without needing classify_rule(). This is the language-agnostic Style IR
438// path — any language that produces StyleFindings gets signal scoring and
439// personality inference for free.
440
441/// Compute signal scores from findings using each finding's `.signal` field
442/// (language-agnostic, no classify_rule() needed).
443pub fn compute_signal_scores_from_findings(
444    findings: &[StyleFinding],
445    total_lines: usize,
446) -> HashMap<StyleSignal, f64> {
447    let k_lines = (total_lines as f64 / 1000.0).max(0.001);
448    let mut counts: HashMap<StyleSignal, usize> = HashMap::new();
449
450    for finding in findings {
451        *counts.entry(finding.signal).or_insert(0) += 1;
452    }
453
454    let mut scores = HashMap::new();
455    for signal in StyleSignal::all() {
456        let count = counts.get(signal).copied().unwrap_or(0);
457        let density = count as f64 / k_lines;
458        let score = ((density + 1.0).log2() * 6.0).min(25.0);
459        scores.insert(*signal, score);
460    }
461
462    scores
463}
464
465/// Build a StyleProfile directly from findings (language-agnostic).
466pub fn build_profile_from_findings(findings: &[StyleFinding], total_lines: usize) -> StyleProfile {
467    let signal_scores = compute_signal_scores_from_findings(findings, total_lines);
468    StyleProfile::from_signal_scores(signal_scores)
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474    use std::path::PathBuf;
475
476    fn make_issue(rule: &str) -> CodeIssue {
477        CodeIssue {
478            file_path: PathBuf::from("src/main.rs"),
479            line: 42,
480            column: 5,
481            rule_name: rule.to_string(),
482            message: format!("{rule} detected"),
483            severity: Severity::Spicy,
484        }
485    }
486
487    // ── FindingId ─────────────────────────────────────────────────
488
489    /// Objective: Verify FindingId is 12-char hex from blake3 hash.
490    #[test]
491    fn test_finding_id_format() {
492        let id = FindingId::new("test-seed");
493        assert_eq!(id.as_str().len(), 12, "id should be 12 hex chars");
494        assert!(id.as_str().chars().all(|c| c.is_ascii_hexdigit()));
495    }
496
497    /// Objective: Verify same seed produces same id.
498    #[test]
499    fn test_finding_id_deterministic() {
500        let a = FindingId::new("hello");
501        let b = FindingId::new("hello");
502        assert_eq!(a, b);
503    }
504
505    /// Objective: Verify different seeds produce different ids.
506    #[test]
507    fn test_finding_id_distinct() {
508        let a = FindingId::new("hello");
509        let b = FindingId::new("world");
510        assert_ne!(a, b);
511    }
512
513    // ── Category mapping ─────────────────────────────────────────
514
515    /// Objective: Verify naming-related rules map to StyleCategory::Naming.
516    #[test]
517    fn test_category_naming() {
518        for name in &[
519            "terrible-naming",
520            "single-letter-variable",
521            "meaningless-naming",
522            "hungarian-notation",
523            "abbreviation-abuse",
524            "go-receiver-name",
525            "ruby-predicate-method",
526        ] {
527            assert_eq!(
528                rule_to_category(name),
529                StyleCategory::Naming,
530                "{name} should be Naming"
531            );
532        }
533    }
534
535    /// Objective: Verify complexity rules map to Complexity.
536    #[test]
537    fn test_category_complexity() {
538        for name in &[
539            "deep-nesting",
540            "cyclomatic-complexity",
541            "complex-closure",
542            "long-function",
543            "god-function",
544            "too-many-params",
545            "module-complexity",
546        ] {
547            assert_eq!(
548                rule_to_category(name),
549                StyleCategory::Complexity,
550                "{name} should be Complexity"
551            );
552        }
553    }
554
555    /// Objective: Verify duplication rules map to Duplication.
556    #[test]
557    fn test_category_duplication() {
558        assert_eq!(
559            rule_to_category("code-duplication"),
560            StyleCategory::Duplication
561        );
562        assert_eq!(
563            rule_to_category("cross-file-duplication"),
564            StyleCategory::Duplication
565        );
566    }
567
568    // ── Confidence mapping ───────────────────────────────────────
569
570    /// Objective: Verify objectively measurable rules return High confidence.
571    #[test]
572    fn test_confidence_high() {
573        for name in &[
574            "magic-number",
575            "code-duplication",
576            "unwrap-abuse",
577            "empty-catch",
578            "dead-code",
579        ] {
580            assert_eq!(
581                rule_to_confidence(name),
582                Confidence::High,
583                "{name} should be High confidence"
584            );
585        }
586    }
587
588    /// Objective: Verify context-dependent rules return Medium confidence.
589    #[test]
590    fn test_confidence_medium() {
591        for name in &[
592            "deep-nesting",
593            "long-function",
594            "todo-comment",
595            "too-many-params",
596        ] {
597            assert_eq!(
598                rule_to_confidence(name),
599                Confidence::Medium,
600                "{name} should be Medium confidence"
601            );
602        }
603    }
604
605    /// Objective: Verify subjective rules return Low confidence.
606    #[test]
607    fn test_confidence_low() {
608        for name in &[
609            "terrible-naming",
610            "single-letter-variable",
611            "abbreviation-abuse",
612            "go-receiver-name",
613        ] {
614            assert_eq!(
615                rule_to_confidence(name),
616                Confidence::Low,
617                "{name} should be Low confidence"
618            );
619        }
620    }
621
622    // ── StyleFinding conversion ──────────────────────────────────
623
624    /// Objective: Verify From<&CodeIssue> produces correct fields.
625    #[test]
626    fn test_finding_from_issue_basic() {
627        let issue = make_issue("unwrap-abuse");
628        let finding = StyleFinding::from(&issue);
629        assert_eq!(finding.location.file_path, PathBuf::from("src/main.rs"));
630        assert_eq!(finding.location.line, 42);
631        assert_eq!(finding.rule.name, "unwrap-abuse");
632        assert_eq!(finding.signal, StyleSignal::PanicAddiction);
633        assert_eq!(finding.severity, Severity::Spicy);
634        assert_eq!(finding.confidence, Confidence::High);
635    }
636
637    /// Objective: Verify finding ID is deterministic for same input.
638    #[test]
639    fn test_finding_id_from_issue_deterministic() {
640        let issue = make_issue("deep-nesting");
641        let a = StyleFinding::from(&issue).id;
642        let b = StyleFinding::from(&issue).id;
643        assert_eq!(a, b, "same issue should produce same finding id");
644    }
645
646    /// Objective: Verify rule_to_category falls back to Consistency for unmatched rules.
647    #[test]
648    fn test_category_fallback_consistency() {
649        let cat = rule_to_category("zzz-unmatched-rule");
650        assert_eq!(cat, StyleCategory::Consistency);
651    }
652
653    /// Objective: Verify rule_to_intent handles known and unknown rules.
654    #[test]
655    fn test_intent_mapping() {
656        assert_eq!(rule_to_intent("deep-nesting"), RuleIntent::CognitiveLoad);
657        assert_eq!(rule_to_intent("unwrap-abuse"), RuleIntent::NoiseReduction);
658        assert_eq!(
659            rule_to_intent("code-duplication"),
660            RuleIntent::Maintainability
661        );
662        assert_eq!(rule_to_intent("terrible-naming"), RuleIntent::Readability);
663    }
664
665    /// Objective: Verify Confidence score values.
666    #[test]
667    fn test_confidence_score_values() {
668        assert!((Confidence::High.score() - 1.0).abs() < f64::EPSILON);
669        assert!((Confidence::Medium.score() - 0.6).abs() < f64::EPSILON);
670        assert!((Confidence::Low.score() - 0.3).abs() < f64::EPSILON);
671    }
672
673    /// Objective: Verify display_name methods return non-empty strings.
674    #[test]
675    fn test_display_names_non_empty() {
676        for cat in &[
677            StyleCategory::Naming,
678            StyleCategory::Complexity,
679            StyleCategory::Duplication,
680            StyleCategory::Comments,
681            StyleCategory::DebuggingLeftovers,
682            StyleCategory::Structure,
683            StyleCategory::Consistency,
684            StyleCategory::DependencyStyle,
685        ] {
686            assert!(
687                !cat.display_name().is_empty(),
688                "{:?} display_name empty",
689                cat
690            );
691        }
692        for intent in &[
693            RuleIntent::Readability,
694            RuleIntent::Maintainability,
695            RuleIntent::TeamConvention,
696            RuleIntent::NoiseReduction,
697            RuleIntent::CognitiveLoad,
698        ] {
699            assert!(
700                !intent.display_name().is_empty(),
701                "{:?} display_name empty",
702                intent
703            );
704        }
705        for conf in &[Confidence::Low, Confidence::Medium, Confidence::High] {
706            assert!(
707                !conf.display_name().is_empty(),
708                "{:?} display_name empty",
709                conf
710            );
711        }
712    }
713
714    /// Objective: Verify evidence snippet comes from issue message.
715    #[test]
716    fn test_evidence_snippet() {
717        let issue = make_issue("deep-nesting");
718        let finding = StyleFinding::from(&issue);
719        assert_eq!(
720            finding.evidence.snippet.as_deref(),
721            Some("deep-nesting detected")
722        );
723    }
724
725    /// Objective: Verify suggestion is None by default for From<CodeIssue>.
726    #[test]
727    fn test_suggestion_default_none() {
728        let issue = make_issue("long-function");
729        let finding = StyleFinding::from(&issue);
730        assert!(finding.suggestion.is_none());
731    }
732
733    // ── Universal Style IR ────────────────────────────────────────
734
735    fn finding(signal: StyleSignal, severity: Severity) -> StyleFinding {
736        let issue = CodeIssue {
737            file_path: PathBuf::from("test.rs"),
738            line: 1,
739            column: 1,
740            rule_name: format!("{:?}", signal).to_lowercase(),
741            message: "test".to_string(),
742            severity,
743        };
744        // Override signal since rule_name won't map correctly
745        let mut f = StyleFinding::from(&issue);
746        f.signal = signal;
747        f
748    }
749
750    /// Objective: Verify compute_signal_scores_from_findings counts signals correctly.
751    #[test]
752    fn test_signal_scores_from_findings() {
753        let findings = vec![
754            finding(StyleSignal::Duplication, Severity::Nuclear),
755            finding(StyleSignal::Duplication, Severity::Spicy),
756            finding(StyleSignal::PanicAddiction, Severity::Mild),
757        ];
758        let scores = compute_signal_scores_from_findings(&findings, 1000);
759        assert!(
760            *scores.get(&StyleSignal::Duplication).unwrap_or(&0.0)
761                > *scores.get(&StyleSignal::PanicAddiction).unwrap_or(&0.0),
762            "Duplication (2 count) should score higher than PanicAddiction (1 count)"
763        );
764    }
765
766    /// Objective: Verify compute_signal_scores_from_findings returns 0 for empty input.
767    #[test]
768    fn test_signal_scores_from_findings_empty() {
769        let scores = compute_signal_scores_from_findings(&[], 1000);
770        for signal in StyleSignal::all() {
771            assert_eq!(
772                scores.get(signal).copied().unwrap_or(0.0),
773                0.0,
774                "empty findings => all signal scores 0"
775            );
776        }
777    }
778
779    /// Objective: Verify build_profile_from_findings produces correct dominant signal.
780    #[test]
781    fn test_build_profile_from_findings_dominant() {
782        let findings = vec![
783            finding(StyleSignal::Duplication, Severity::Nuclear),
784            finding(StyleSignal::Duplication, Severity::Nuclear),
785            finding(StyleSignal::Duplication, Severity::Nuclear),
786            finding(StyleSignal::NestedHell, Severity::Mild),
787        ];
788        let profile = build_profile_from_findings(&findings, 1000);
789        assert_eq!(
790            profile.dominant_signal,
791            Some(StyleSignal::Duplication),
792            "3 duplicates vs 1 nested => Duplication should be dominant"
793        );
794    }
795
796    /// Objective: Verify build_profile_from_findings allows personality inference.
797    #[test]
798    fn test_build_profile_from_findings_personality() {
799        let findings = vec![
800            finding(StyleSignal::Duplication, Severity::Nuclear),
801            finding(StyleSignal::Duplication, Severity::Nuclear),
802            finding(StyleSignal::Duplication, Severity::Nuclear),
803            finding(StyleSignal::Duplication, Severity::Nuclear),
804            finding(StyleSignal::Duplication, Severity::Nuclear),
805            finding(StyleSignal::Duplication, Severity::Nuclear),
806            finding(StyleSignal::Duplication, Severity::Nuclear),
807            finding(StyleSignal::Duplication, Severity::Nuclear),
808        ];
809        let profile = build_profile_from_findings(&findings, 100);
810        let personality = profile.infer_personality_type();
811        assert_eq!(
812            personality, "The Copy-Paste Artist",
813            "massive duplication => Copy-Paste Artist, got {personality}"
814        );
815    }
816}