Skip to main content

batuta/bug_hunter/
types.rs

1//! Bug Hunter Types
2//!
3//! Types for representing bug hunting results, findings, and hunt configurations.
4//!
5//! # Popperian Philosophy
6//!
7//! Bug hunting operationalizes falsification: we systematically attempt to break
8//! code, not merely verify it works. Each finding represents a successful
9//! falsification of the implicit claim "this code is correct."
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15/// Mode of bug hunting operation.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum HuntMode {
18    /// Mutation-based invariant falsification (FDV pattern)
19    Falsify,
20    /// SBFL without failing tests (SBEST pattern)
21    Hunt,
22    /// LLM-augmented static analysis (LLIFT pattern)
23    Analyze,
24    /// Targeted unsafe Rust fuzzing (FourFuzz pattern)
25    Fuzz,
26    /// Hybrid concolic + SBFL (COTTONTAIL pattern)
27    DeepHunt,
28    /// Quick pattern-only scan (no clippy, no coverage)
29    Quick,
30}
31
32impl std::fmt::Display for HuntMode {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            HuntMode::Falsify => write!(f, "Falsify"),
36            HuntMode::Hunt => write!(f, "Hunt"),
37            HuntMode::Analyze => write!(f, "Analyze"),
38            HuntMode::Fuzz => write!(f, "Fuzz"),
39            HuntMode::DeepHunt => write!(f, "DeepHunt"),
40            HuntMode::Quick => write!(f, "Quick"),
41        }
42    }
43}
44
45/// Severity of a bug finding.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
47pub enum FindingSeverity {
48    /// Informational finding
49    Info,
50    /// Low severity - style or minor issue
51    Low,
52    /// Medium severity - potential bug
53    Medium,
54    /// High severity - likely bug
55    High,
56    /// Critical - security vulnerability or crash
57    Critical,
58}
59
60impl std::fmt::Display for FindingSeverity {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            FindingSeverity::Info => write!(f, "INFO"),
64            FindingSeverity::Low => write!(f, "LOW"),
65            FindingSeverity::Medium => write!(f, "MEDIUM"),
66            FindingSeverity::High => write!(f, "HIGH"),
67            FindingSeverity::Critical => write!(f, "CRITICAL"),
68        }
69    }
70}
71
72/// Category of defect (aligned with OIP categories).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub enum DefectCategory {
75    /// Missing or incorrect trait bounds
76    TraitBounds,
77    /// Syntax/structure issues, macro expansion bugs
78    AstTransform,
79    /// Ownership/lifetime errors
80    OwnershipBorrow,
81    /// Config/environment issues
82    ConfigurationErrors,
83    /// Race conditions, data races
84    ConcurrencyBugs,
85    /// Security issues
86    SecurityVulnerabilities,
87    /// Type mismatches
88    TypeErrors,
89    /// Memory bugs (use-after-free, etc.)
90    MemorySafety,
91    /// Logic errors
92    LogicErrors,
93    /// Performance issues
94    PerformanceIssues,
95    /// GPU/CUDA kernel bugs (PTX, memory access, dimension limits)
96    GpuKernelBugs,
97    /// Silent degradation (fallbacks that hide failures)
98    SilentDegradation,
99    /// Test debt (skipped/ignored tests indicating known bugs)
100    TestDebt,
101    /// Hidden debt (euphemisms like 'placeholder', 'stub', 'demo')
102    HiddenDebt,
103    /// Contract verification gap (BH-26: missing proof, partial binding)
104    ContractGap,
105    /// Model parity gap (BH-27: oracle mismatch, quantization drift)
106    ModelParityGap,
107    /// Unknown/uncategorized
108    Unknown,
109}
110
111impl std::fmt::Display for DefectCategory {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            DefectCategory::TraitBounds => write!(f, "TraitBounds"),
115            DefectCategory::AstTransform => write!(f, "ASTTransform"),
116            DefectCategory::OwnershipBorrow => write!(f, "OwnershipBorrow"),
117            DefectCategory::ConfigurationErrors => write!(f, "ConfigurationErrors"),
118            DefectCategory::ConcurrencyBugs => write!(f, "ConcurrencyBugs"),
119            DefectCategory::SecurityVulnerabilities => write!(f, "SecurityVulnerabilities"),
120            DefectCategory::TypeErrors => write!(f, "TypeErrors"),
121            DefectCategory::MemorySafety => write!(f, "MemorySafety"),
122            DefectCategory::LogicErrors => write!(f, "LogicErrors"),
123            DefectCategory::PerformanceIssues => write!(f, "PerformanceIssues"),
124            DefectCategory::GpuKernelBugs => write!(f, "GpuKernelBugs"),
125            DefectCategory::SilentDegradation => write!(f, "SilentDegradation"),
126            DefectCategory::TestDebt => write!(f, "TestDebt"),
127            DefectCategory::HiddenDebt => write!(f, "HiddenDebt"),
128            DefectCategory::ContractGap => write!(f, "ContractGap"),
129            DefectCategory::ModelParityGap => write!(f, "ModelParityGap"),
130            DefectCategory::Unknown => write!(f, "Unknown"),
131        }
132    }
133}
134
135/// A single bug finding.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct Finding {
138    /// Unique identifier
139    pub id: String,
140
141    /// File path where the finding was located
142    pub file: PathBuf,
143
144    /// Line number (1-indexed)
145    pub line: usize,
146
147    /// Column number (1-indexed, optional)
148    pub column: Option<usize>,
149
150    /// Finding title
151    pub title: String,
152
153    /// Detailed description
154    pub description: String,
155
156    /// Severity level
157    pub severity: FindingSeverity,
158
159    /// Defect category
160    pub category: DefectCategory,
161
162    /// Suspiciousness score (0.0 - 1.0)
163    pub suspiciousness: f64,
164
165    /// Hunt mode that discovered this finding
166    pub discovered_by: HuntMode,
167
168    /// Evidence supporting the finding
169    pub evidence: Vec<FindingEvidence>,
170
171    /// Suggested fix (if available)
172    pub suggested_fix: Option<String>,
173
174    /// Related findings (by ID)
175    pub related: Vec<String>,
176
177    /// Regression risk score (0.0 - 1.0) from PMAT quality data (BH-24)
178    #[serde(default)]
179    pub regression_risk: Option<f64>,
180
181    /// Git blame information: author name
182    #[serde(default)]
183    pub blame_author: Option<String>,
184
185    /// Git blame information: commit hash (short)
186    #[serde(default)]
187    pub blame_commit: Option<String>,
188
189    /// Git blame information: date of last change
190    #[serde(default)]
191    pub blame_date: Option<String>,
192}
193
194impl Finding {
195    /// Create a new finding.
196    pub fn new(
197        id: impl Into<String>,
198        file: impl Into<PathBuf>,
199        line: usize,
200        title: impl Into<String>,
201    ) -> Self {
202        Self {
203            id: id.into(),
204            file: file.into(),
205            line,
206            column: None,
207            title: title.into(),
208            description: String::new(),
209            severity: FindingSeverity::Medium,
210            category: DefectCategory::Unknown,
211            suspiciousness: 0.5,
212            discovered_by: HuntMode::Analyze,
213            evidence: Vec::new(),
214            suggested_fix: None,
215            related: Vec::new(),
216            regression_risk: None,
217            blame_author: None,
218            blame_commit: None,
219            blame_date: None,
220        }
221    }
222
223    /// Set column.
224    pub fn with_column(mut self, column: usize) -> Self {
225        self.column = Some(column);
226        self
227    }
228
229    /// Set description.
230    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
231        self.description = desc.into();
232        self
233    }
234
235    /// Set severity.
236    pub fn with_severity(mut self, severity: FindingSeverity) -> Self {
237        self.severity = severity;
238        self
239    }
240
241    /// Set category.
242    pub fn with_category(mut self, category: DefectCategory) -> Self {
243        self.category = category;
244        self
245    }
246
247    /// Set suspiciousness score.
248    pub fn with_suspiciousness(mut self, score: f64) -> Self {
249        self.suspiciousness = score.clamp(0.0, 1.0);
250        self
251    }
252
253    /// Set discovery mode.
254    pub fn with_discovered_by(mut self, mode: HuntMode) -> Self {
255        self.discovered_by = mode;
256        self
257    }
258
259    /// Add evidence.
260    pub fn with_evidence(mut self, evidence: FindingEvidence) -> Self {
261        self.evidence.push(evidence);
262        self
263    }
264
265    /// Set suggested fix.
266    pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
267        self.suggested_fix = Some(fix.into());
268        self
269    }
270
271    /// Set regression risk score (BH-24).
272    pub fn with_regression_risk(mut self, risk: f64) -> Self {
273        self.regression_risk = Some(risk.clamp(0.0, 1.0));
274        self
275    }
276
277    /// Set git blame information.
278    pub fn with_blame(
279        mut self,
280        author: impl Into<String>,
281        commit: impl Into<String>,
282        date: impl Into<String>,
283    ) -> Self {
284        self.blame_author = Some(author.into());
285        self.blame_commit = Some(commit.into());
286        self.blame_date = Some(date.into());
287        self
288    }
289
290    /// Get location string.
291    pub fn location(&self) -> String {
292        match self.column {
293            Some(col) => format!("{}:{}:{}", self.file.display(), self.line, col),
294            None => format!("{}:{}", self.file.display(), self.line),
295        }
296    }
297}
298
299/// Evidence supporting a finding.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct FindingEvidence {
302    /// Evidence type
303    pub evidence_type: EvidenceKind,
304
305    /// Description
306    pub description: String,
307
308    /// Raw data
309    pub data: Option<String>,
310}
311
312impl FindingEvidence {
313    /// Create mutation evidence.
314    pub fn mutation(mutant_id: impl Into<String>, survived: bool) -> Self {
315        Self {
316            evidence_type: EvidenceKind::MutationSurvival,
317            description: format!(
318                "Mutant {} {}",
319                mutant_id.into(),
320                if survived { "SURVIVED" } else { "KILLED" }
321            ),
322            data: Some(if survived { "SURVIVED".into() } else { "KILLED".into() }),
323        }
324    }
325
326    /// Create SBFL evidence.
327    pub fn sbfl(formula: impl Into<String>, score: f64) -> Self {
328        Self {
329            evidence_type: EvidenceKind::SbflScore,
330            description: format!("{} suspiciousness: {:.3}", formula.into(), score),
331            data: Some(format!("{:.6}", score)),
332        }
333    }
334
335    /// Create static analysis evidence.
336    pub fn static_analysis(tool: impl Into<String>, message: impl Into<String>) -> Self {
337        Self {
338            evidence_type: EvidenceKind::StaticAnalysis,
339            description: format!("[{}] {}", tool.into(), message.into()),
340            data: None,
341        }
342    }
343
344    /// Create fuzzing evidence.
345    pub fn fuzzing(input: impl Into<String>, crash_type: impl Into<String>) -> Self {
346        Self {
347            evidence_type: EvidenceKind::FuzzingCrash,
348            description: crash_type.into(),
349            data: Some(input.into()),
350        }
351    }
352
353    /// Create quality metrics evidence (BH-21).
354    pub fn quality_metrics(grade: impl Into<String>, tdg_score: f64, complexity: u32) -> Self {
355        Self {
356            evidence_type: EvidenceKind::QualityMetrics,
357            description: format!(
358                "PMAT grade {} (TDG: {:.1}, complexity: {})",
359                grade.into(),
360                tdg_score,
361                complexity
362            ),
363            data: Some(format!("{:.1}", tdg_score)),
364        }
365    }
366
367    /// Create contract binding evidence (BH-26).
368    pub fn contract_binding(
369        contract: impl Into<String>,
370        equation: impl Into<String>,
371        status: impl Into<String>,
372    ) -> Self {
373        Self {
374            evidence_type: EvidenceKind::ContractBinding,
375            description: format!(
376                "Contract {} eq {} — {}",
377                contract.into(),
378                equation.into(),
379                status.into()
380            ),
381            data: None,
382        }
383    }
384
385    /// Create model parity evidence (BH-27).
386    pub fn model_parity(
387        model: impl Into<String>,
388        check: impl Into<String>,
389        result: impl Into<String>,
390    ) -> Self {
391        Self {
392            evidence_type: EvidenceKind::ModelParity,
393            description: format!("Model {} — {} — {}", model.into(), check.into(), result.into()),
394            data: None,
395        }
396    }
397
398    /// Create concolic evidence.
399    pub fn concolic(path_constraint: impl Into<String>) -> Self {
400        Self {
401            evidence_type: EvidenceKind::ConcolicPath,
402            description: "Path constraint solved".into(),
403            data: Some(path_constraint.into()),
404        }
405    }
406}
407
408/// Kind of evidence.
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
410pub enum EvidenceKind {
411    /// Mutation survived (test weakness)
412    MutationSurvival,
413    /// SBFL suspiciousness score
414    SbflScore,
415    /// Static analysis warning
416    StaticAnalysis,
417    /// Fuzzing crash/violation
418    FuzzingCrash,
419    /// Concolic execution path
420    ConcolicPath,
421    /// LLM classification
422    LlmClassification,
423    /// Git history correlation
424    GitHistory,
425    /// PMAT quality metrics (BH-21)
426    QualityMetrics,
427    /// Contract binding status (BH-26)
428    ContractBinding,
429    /// Model parity result (BH-27)
430    ModelParity,
431}
432
433/// Configuration for a bug hunt.
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct HuntConfig {
436    /// Hunt mode
437    pub mode: HuntMode,
438
439    /// Target paths to analyze
440    pub targets: Vec<PathBuf>,
441
442    /// Minimum suspiciousness threshold
443    pub min_suspiciousness: f64,
444
445    /// Maximum findings to report
446    pub max_findings: usize,
447
448    /// SBFL formula (for Hunt/DeepHunt modes)
449    pub sbfl_formula: SbflFormula,
450
451    /// Enable LLM filtering
452    pub llm_filter: bool,
453
454    /// Fuzzing duration in seconds (for Fuzz mode)
455    pub fuzz_duration_secs: u64,
456
457    /// Mutation timeout in seconds (for Falsify mode)
458    pub mutation_timeout_secs: u64,
459
460    /// Categories to include (empty = all)
461    pub include_categories: Vec<DefectCategory>,
462
463    /// Categories to exclude
464    pub exclude_categories: Vec<DefectCategory>,
465
466    // =========================================================================
467    // BH-11 to BH-15: Advanced Features
468    // =========================================================================
469    /// Spec file path (BH-11: Spec-Driven Bug Hunting)
470    pub spec_path: Option<PathBuf>,
471
472    /// Spec section filter (e.g., "Authentication")
473    pub spec_section: Option<String>,
474
475    /// PMAT ticket reference (BH-12: Ticket Integration)
476    pub ticket_ref: Option<String>,
477
478    /// Update spec with findings (BH-14: Bidirectional Linking)
479    pub update_spec: bool,
480
481    /// Analyze library only (BH-13: Scoped Analysis)
482    pub lib_only: bool,
483
484    /// Analyze specific binary (BH-13: Scoped Analysis)
485    pub bin_target: Option<String>,
486
487    /// Exclude test code (BH-13: Scoped Analysis)
488    pub exclude_tests: bool,
489
490    /// Suppress known false positive patterns (BH-15)
491    pub suppress_false_positives: bool,
492
493    /// Custom coverage data path (lcov.info)
494    pub coverage_path: Option<PathBuf>,
495
496    /// Coverage weight factor for hotpath weighting (default 0.5)
497    pub coverage_weight: f64,
498
499    // =========================================================================
500    // BH-16 to BH-20: Research-Based Fault Localization
501    // =========================================================================
502    /// Fault localization strategy (BH-16 to BH-19)
503    pub localization_strategy: LocalizationStrategy,
504
505    /// Channel weights for multi-channel localization (BH-19)
506    pub channel_weights: ChannelWeights,
507
508    /// Enable predictive mutation testing (BH-18)
509    pub predictive_mutation: bool,
510
511    /// Enable semantic crash bucketing (BH-20)
512    pub crash_bucketing: CrashBucketingMode,
513
514    // =========================================================================
515    // BH-21 to BH-25: PMAT Quality Integration
516    // =========================================================================
517    /// Enable PMAT quality-weighted suspiciousness (BH-21)
518    pub use_pmat_quality: bool,
519
520    /// Quality weight factor for suspiciousness adjustment (BH-21, default 0.5)
521    pub quality_weight: f64,
522
523    /// Use PMAT to scope targets by quality (BH-22)
524    pub pmat_scope: bool,
525
526    /// Enable SATD-enriched findings from PMAT (BH-23, default true)
527    pub pmat_satd: bool,
528
529    /// PMAT query string for scoping (BH-22)
530    pub pmat_query: Option<String>,
531
532    // =========================================================================
533    // BH-26 to BH-27: Contract & Model Parity Analysis
534    // =========================================================================
535    /// Explicit path to provable-contracts directory (BH-26)
536    pub contracts_path: Option<PathBuf>,
537
538    /// Auto-discover provable-contracts in sibling directories (BH-26)
539    pub contracts_auto: bool,
540
541    /// Explicit path to tiny-model-ground-truth directory (BH-27)
542    pub model_parity_path: Option<PathBuf>,
543
544    /// Auto-discover tiny-model-ground-truth in sibling directories (BH-27)
545    pub model_parity_auto: bool,
546}
547
548/// Crash bucketing mode (BH-20).
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
550pub enum CrashBucketingMode {
551    /// No bucketing
552    #[default]
553    None,
554    /// Stack trace similarity only
555    StackTrace,
556    /// Semantic root cause analysis
557    Semantic,
558}
559
560impl Default for HuntConfig {
561    fn default() -> Self {
562        Self {
563            mode: HuntMode::Analyze,
564            targets: vec![PathBuf::from("src")],
565            min_suspiciousness: 0.5,
566            max_findings: 50,
567            sbfl_formula: SbflFormula::Ochiai,
568            llm_filter: false,
569            fuzz_duration_secs: 60,
570            mutation_timeout_secs: 30,
571            include_categories: Vec::new(),
572            exclude_categories: Vec::new(),
573            // BH-11 to BH-15 defaults
574            spec_path: None,
575            spec_section: None,
576            ticket_ref: None,
577            update_spec: false,
578            lib_only: false,
579            bin_target: None,
580            exclude_tests: true,            // Default to excluding tests
581            suppress_false_positives: true, // Default to suppressing
582            coverage_path: None,
583            coverage_weight: 0.5, // Default hotpath weight
584            // BH-16 to BH-20 defaults
585            localization_strategy: LocalizationStrategy::default(),
586            channel_weights: ChannelWeights::default(),
587            predictive_mutation: false,
588            crash_bucketing: CrashBucketingMode::default(),
589            // BH-21 to BH-25 defaults
590            use_pmat_quality: false,
591            quality_weight: 0.5,
592            pmat_scope: false,
593            pmat_satd: true,
594            pmat_query: None,
595            // BH-26 to BH-27 defaults
596            contracts_path: None,
597            contracts_auto: false,
598            model_parity_path: None,
599            model_parity_auto: false,
600        }
601    }
602}
603
604/// SBFL formula for fault localization.
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
606pub enum SbflFormula {
607    /// Tarantula formula (classic)
608    Tarantula,
609    /// Ochiai formula (cosine similarity)
610    #[default]
611    Ochiai,
612    /// DStar with power 2
613    DStar2,
614    /// DStar with power 3
615    DStar3,
616}
617
618/// Fault localization strategy (BH-16 to BH-19).
619#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
620pub enum LocalizationStrategy {
621    /// Spectrum-Based Fault Localization only
622    #[default]
623    Sbfl,
624    /// Mutation-Based Fault Localization (BH-16)
625    Mbfl,
626    /// Causal inference with interventions (BH-17)
627    Causal,
628    /// Multi-channel combination (BH-19)
629    MultiChannel,
630    /// Hybrid SBFL + MBFL with configurable weights
631    Hybrid,
632}
633
634impl std::fmt::Display for LocalizationStrategy {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        match self {
637            Self::Sbfl => write!(f, "SBFL"),
638            Self::Mbfl => write!(f, "MBFL"),
639            Self::Causal => write!(f, "Causal"),
640            Self::MultiChannel => write!(f, "MultiChannel"),
641            Self::Hybrid => write!(f, "Hybrid"),
642        }
643    }
644}
645
646/// Multi-channel weights for fault localization (BH-19, BH-21).
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct ChannelWeights {
649    /// SBFL spectrum-based weight
650    pub spectrum: f64,
651    /// MBFL mutation-based weight
652    pub mutation: f64,
653    /// Static analysis weight (clippy/patterns)
654    pub static_analysis: f64,
655    /// Semantic similarity weight (error message matching)
656    pub semantic: f64,
657    /// PMAT quality weight (BH-21)
658    #[serde(default)]
659    pub quality: f64,
660}
661
662impl Default for ChannelWeights {
663    fn default() -> Self {
664        Self {
665            spectrum: 0.30,
666            mutation: 0.25,
667            static_analysis: 0.20,
668            semantic: 0.15,
669            quality: 0.10,
670        }
671    }
672}
673
674impl ChannelWeights {
675    /// Normalize weights to sum to 1.0
676    pub fn normalize(&mut self) {
677        let sum =
678            self.spectrum + self.mutation + self.static_analysis + self.semantic + self.quality;
679        if sum > 0.0 {
680            self.spectrum /= sum;
681            self.mutation /= sum;
682            self.static_analysis /= sum;
683            self.semantic /= sum;
684            self.quality /= sum;
685        }
686    }
687
688    /// Compute weighted score from channel scores (5 channels)
689    pub fn combine(
690        &self,
691        spectrum: f64,
692        mutation: f64,
693        static_score: f64,
694        semantic: f64,
695        quality: f64,
696    ) -> f64 {
697        self.spectrum * spectrum
698            + self.mutation * mutation
699            + self.static_analysis * static_score
700            + self.semantic * semantic
701            + self.quality * quality
702    }
703}
704
705impl std::fmt::Display for SbflFormula {
706    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
707        match self {
708            SbflFormula::Tarantula => write!(f, "Tarantula"),
709            SbflFormula::Ochiai => write!(f, "Ochiai"),
710            SbflFormula::DStar2 => write!(f, "DStar2"),
711            SbflFormula::DStar3 => write!(f, "DStar3"),
712        }
713    }
714}
715
716/// Result of a bug hunt.
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct HuntResult {
719    /// Project path analyzed
720    pub project_path: PathBuf,
721
722    /// Hunt mode used
723    pub mode: HuntMode,
724
725    /// Configuration used
726    pub config: HuntConfig,
727
728    /// Findings discovered
729    pub findings: Vec<Finding>,
730
731    /// Statistics
732    pub stats: HuntStats,
733
734    /// Timestamp
735    pub timestamp: String,
736
737    /// Duration in milliseconds
738    pub duration_ms: u64,
739
740    /// Phase timing breakdown
741    #[serde(default)]
742    pub phase_timings: PhaseTimings,
743}
744
745impl HuntResult {
746    /// Create a new hunt result.
747    pub fn new(project_path: impl Into<PathBuf>, mode: HuntMode, config: HuntConfig) -> Self {
748        Self {
749            project_path: project_path.into(),
750            mode,
751            config,
752            findings: Vec::new(),
753            stats: HuntStats::default(),
754            timestamp: chrono::Utc::now().to_rfc3339(),
755            duration_ms: 0,
756            phase_timings: PhaseTimings::default(),
757        }
758    }
759
760    /// Add a finding.
761    pub fn add_finding(&mut self, finding: Finding) {
762        self.findings.push(finding);
763    }
764
765    /// Set duration.
766    pub fn with_duration(mut self, ms: u64) -> Self {
767        self.duration_ms = ms;
768        self
769    }
770
771    /// Finalize statistics.
772    pub fn finalize(&mut self) {
773        self.stats = HuntStats::from_findings(&self.findings);
774    }
775
776    /// Get findings sorted by suspiciousness (descending).
777    pub fn top_findings(&self, n: usize) -> Vec<&Finding> {
778        let mut sorted: Vec<_> = self.findings.iter().collect();
779        sorted.sort_by(|a, b| {
780            b.suspiciousness.partial_cmp(&a.suspiciousness).unwrap_or(std::cmp::Ordering::Equal)
781        });
782        sorted.into_iter().take(n).collect()
783    }
784
785    /// Get summary string.
786    pub fn summary(&self) -> String {
787        let c = self.stats.by_severity.get(&FindingSeverity::Critical).unwrap_or(&0);
788        let h = self.stats.by_severity.get(&FindingSeverity::High).unwrap_or(&0);
789        format!(
790            "{} mode: {} findings in {} files ({}C {}H) -- {}ms",
791            self.mode,
792            self.findings.len(),
793            self.stats.files_analyzed,
794            c,
795            h,
796            self.duration_ms
797        )
798    }
799}
800
801impl Default for HuntResult {
802    fn default() -> Self {
803        Self {
804            project_path: PathBuf::new(),
805            mode: HuntMode::Quick,
806            config: HuntConfig::default(),
807            findings: Vec::new(),
808            stats: HuntStats::default(),
809            timestamp: String::new(),
810            duration_ms: 0,
811            phase_timings: PhaseTimings::default(),
812        }
813    }
814}
815
816/// Statistics from a bug hunt.
817#[derive(Debug, Clone, Default, Serialize, Deserialize)]
818pub struct HuntStats {
819    /// Total findings
820    pub total_findings: usize,
821
822    /// Findings by severity
823    pub by_severity: HashMap<FindingSeverity, usize>,
824
825    /// Findings by category
826    pub by_category: HashMap<DefectCategory, usize>,
827
828    /// Files analyzed
829    pub files_analyzed: usize,
830
831    /// Lines analyzed
832    pub lines_analyzed: usize,
833
834    /// Average suspiciousness
835    pub avg_suspiciousness: f64,
836
837    /// Max suspiciousness
838    pub max_suspiciousness: f64,
839
840    /// Mode-specific stats
841    pub mode_stats: ModeStats,
842}
843
844impl HuntStats {
845    /// Compute statistics from findings.
846    pub fn from_findings(findings: &[Finding]) -> Self {
847        let mut by_severity: HashMap<FindingSeverity, usize> = HashMap::new();
848        let mut by_category: HashMap<DefectCategory, usize> = HashMap::new();
849        let mut total_suspiciousness = 0.0;
850        let mut max_suspiciousness = 0.0;
851        let mut unique_files: std::collections::HashSet<&std::path::Path> =
852            std::collections::HashSet::new();
853
854        for finding in findings {
855            *by_severity.entry(finding.severity).or_default() += 1;
856            *by_category.entry(finding.category).or_default() += 1;
857            total_suspiciousness += finding.suspiciousness;
858            if finding.suspiciousness > max_suspiciousness {
859                max_suspiciousness = finding.suspiciousness;
860            }
861            unique_files.insert(&finding.file);
862        }
863
864        Self {
865            total_findings: findings.len(),
866            by_severity,
867            by_category,
868            files_analyzed: unique_files.len(),
869            avg_suspiciousness: if findings.is_empty() {
870                0.0
871            } else {
872                total_suspiciousness / findings.len() as f64
873            },
874            max_suspiciousness,
875            ..Default::default()
876        }
877    }
878}
879
880/// Phase timing breakdown for bug-hunter pipeline.
881#[derive(Debug, Clone, Default, Serialize, Deserialize)]
882pub struct PhaseTimings {
883    /// Mode dispatch (scanning) phase duration in ms
884    pub mode_dispatch_ms: u64,
885    /// PMAT index construction duration in ms
886    pub pmat_index_ms: u64,
887    /// PMAT weight application duration in ms
888    pub pmat_weights_ms: u64,
889    /// Finalization phase duration in ms
890    pub finalize_ms: u64,
891    /// Contract gap analysis duration in ms (BH-26)
892    pub contract_gap_ms: u64,
893    /// Model parity analysis duration in ms (BH-27)
894    pub model_parity_ms: u64,
895}
896
897/// Mode-specific statistics.
898#[derive(Debug, Clone, Default, Serialize, Deserialize)]
899pub struct ModeStats {
900    /// Mutation testing: total mutants
901    pub mutants_total: usize,
902    /// Mutation testing: killed mutants
903    pub mutants_killed: usize,
904    /// Mutation testing: survived mutants
905    pub mutants_survived: usize,
906
907    /// SBFL: passing tests
908    pub sbfl_passing_tests: usize,
909    /// SBFL: failing tests
910    pub sbfl_failing_tests: usize,
911
912    /// Fuzzing: total executions
913    pub fuzz_executions: usize,
914    /// Fuzzing: crashes found
915    pub fuzz_crashes: usize,
916    /// Fuzzing: coverage percentage
917    pub fuzz_coverage: f64,
918
919    /// Concolic: paths explored
920    pub concolic_paths: usize,
921    /// Concolic: constraints solved
922    pub concolic_constraints_solved: usize,
923    /// Concolic: timeouts
924    pub concolic_timeouts: usize,
925
926    /// LLM: warnings filtered
927    pub llm_filtered: usize,
928    /// LLM: true positives retained
929    pub llm_retained: usize,
930}
931
932#[cfg(test)]
933#[path = "types_tests.rs"]
934mod tests;