Skip to main content

veritas_plugin_api/
lib.rs

1use std::{collections::BTreeMap, path::Path};
2
3use anyhow::Result;
4use camino::Utf8PathBuf;
5use serde::{Deserialize, Serialize};
6
7pub trait LanguagePlugin: Send + Sync {
8    fn id(&self) -> &'static str;
9
10    fn display_name(&self) -> &'static str;
11
12    fn capabilities(&self) -> Vec<PluginCapability> {
13        vec![
14            PluginCapability::TargetDiscovery,
15            PluginCapability::GeneratedTests,
16            PluginCapability::ExistingTests,
17            PluginCapability::Coverage,
18            PluginCapability::RegressionPromotion,
19        ]
20    }
21
22    fn detect_project(&self, root: &Path) -> Result<ProjectInfo>;
23
24    fn discover_targets(&self, root: &Path) -> Result<Vec<VerificationTarget>>;
25
26    fn generate_tests(
27        &self,
28        target: &VerificationTarget,
29        plan: &VerificationPlan,
30    ) -> Result<Vec<GeneratedArtifact>>;
31
32    fn run_tests(
33        &self,
34        root: &Path,
35        artifacts: &[GeneratedArtifact],
36        plan: &VerificationPlan,
37    ) -> Result<TestRunResult>;
38
39    fn collect_coverage(&self, root: &Path) -> Result<Option<CoverageReport>>;
40
41    fn replay_behavior(
42        &self,
43        _root: &Path,
44        _target: &VerificationTarget,
45        _case: &BehaviorReplayCase,
46    ) -> Result<Option<BehaviorReplayObservation>> {
47        Ok(None)
48    }
49
50    fn replay_behaviors(
51        &self,
52        root: &Path,
53        target: &VerificationTarget,
54        cases: &[BehaviorReplayCase],
55    ) -> Result<BTreeMap<String, BehaviorReplayObservation>> {
56        let mut observations = BTreeMap::new();
57        for case in cases {
58            if let Some(observation) = self.replay_behavior(root, target, case)? {
59                observations.insert(case.name.clone(), observation);
60            }
61        }
62        Ok(observations)
63    }
64
65    fn promote_regression(
66        &self,
67        _root: &Path,
68        _report: &VerificationReport,
69        finding: &Failure,
70        index: usize,
71    ) -> Result<Vec<GeneratedArtifact>> {
72        let language = finding_language(finding).unwrap_or_else(|| self.id().to_string());
73        let mut contents = String::from("# Regression Promotion\n\n");
74        contents.push_str("Generated by veritas. Review before committing.\n\n");
75        contents.push_str(&format!("- Finding: {}\n", finding.message));
76        contents.push_str(&format!("- Severity: {:?}\n", finding.severity));
77        contents.push_str(&format!("- Command: `{}`\n", finding.command));
78        if let Some(target_id) = &finding.target_id {
79            contents.push_str(&format!("- Target: `{target_id}`\n"));
80        }
81        if let Some(repro) = &finding.repro {
82            contents.push_str(&format!("- Repro command: `{}`\n", repro.command));
83            if let Some(path) = &repro.path {
84                contents.push_str(&format!("- Repro path: `{path}`\n"));
85            }
86            if let Some(input) = &repro.input {
87                contents.push_str(&format!("- Repro input: `{}`\n", input.trim()));
88            }
89        }
90        contents.push_str("\n## Next Step\n\n");
91        contents.push_str("This language plugin has not implemented executable regression promotion yet. Add a handwritten test owned by the target package, then rerun `veritas verify`.\n");
92
93        Ok(vec![GeneratedArtifact {
94            id: format!("{language}-promoted-regression-{index}"),
95            language: language.clone(),
96            kind: ArtifactKind::RegressionTest,
97            target_id: finding
98                .target_id
99                .clone()
100                .unwrap_or_else(|| format!("{language}:unknown")),
101            path: Utf8PathBuf::from(format!(
102                ".veritas/regressions/promoted/{language}_{index}.md"
103            )),
104            contents,
105            description: "Generic regression promotion guidance".to_string(),
106            status: ArtifactStatus::Planned,
107        }])
108    }
109}
110
111fn finding_language(finding: &Failure) -> Option<String> {
112    finding
113        .target_id
114        .as_deref()
115        .and_then(|target_id| target_id.split_once(':').map(|(language, _)| language))
116        .map(ToString::to_string)
117}
118
119pub trait VerificationPlanner: Send + Sync {
120    fn plan(&self, project: &ProjectInfo, target: &VerificationTarget) -> Result<VerificationPlan>;
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124pub struct ProjectInfo {
125    pub language: String,
126    pub name: String,
127    pub root: Utf8PathBuf,
128    pub manifests: Vec<Utf8PathBuf>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub struct VerificationTarget {
133    pub id: String,
134    pub language: String,
135    pub kind: TargetKind,
136    pub path: Utf8PathBuf,
137    pub symbol: Option<String>,
138    pub signature: Option<String>,
139    pub line_range: Option<LineRange>,
140    pub description: String,
141    pub risk: RiskLevel,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145pub struct LineRange {
146    pub start: usize,
147    pub end: usize,
148}
149
150impl LineRange {
151    pub fn overlaps(&self, other: &LineRange) -> bool {
152        self.start <= other.end && other.start <= self.end
153    }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
157#[serde(rename_all = "snake_case")]
158pub enum TargetKind {
159    Project,
160    Package,
161    File,
162    Function,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
166#[serde(rename_all = "snake_case")]
167pub enum RiskLevel {
168    Low,
169    Medium,
170    High,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
174#[serde(rename_all = "snake_case")]
175pub enum FailureSeverity {
176    Info,
177    Warning,
178    Error,
179    Critical,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183pub struct VerificationPlan {
184    pub target_id: String,
185    pub strategies: Vec<VerificationStrategy>,
186    pub budget_seconds: u64,
187    pub write_generated_tests: bool,
188    pub run_existing_tests: bool,
189    pub run_generated_tests: bool,
190    pub fail_on_generated_test_failure: bool,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
194#[serde(rename_all = "snake_case")]
195pub enum VerificationStrategy {
196    ExistingTests,
197    UnitTests,
198    PropertyTests,
199    Fuzzing,
200    DifferentialTests,
201    MutationChecks,
202    CoverageFeedback,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206pub struct GeneratedArtifact {
207    pub id: String,
208    pub language: String,
209    pub kind: ArtifactKind,
210    pub target_id: String,
211    pub path: Utf8PathBuf,
212    pub contents: String,
213    pub description: String,
214    pub status: ArtifactStatus,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218#[serde(rename_all = "snake_case")]
219pub enum ArtifactKind {
220    UnitTest,
221    PropertyTest,
222    FuzzHarness,
223    HarnessIndex,
224    MutationCheck,
225    CoverageFeedback,
226    DifferentialBaseline,
227    ReproCase,
228    PackageAwareness,
229    PackageGraph,
230    SymbolGraph,
231    ChangeDigest,
232    AiFeedback,
233    CandidatePatch,
234    FindingBaseline,
235    RegressionTest,
236    DifferentialReplay,
237    EvolutionPlan,
238    AssertionCandidate,
239    CorpusEntry,
240    ReplayResult,
241    BudgetPlan,
242    ConfidenceScore,
243    MutationTrend,
244    MutationCampaign,
245    EvolutionCandidate,
246    EvolutionSuite,
247    CorpusReplay,
248    TargetCache,
249    SiteAsset,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
253#[serde(rename_all = "snake_case")]
254pub enum PluginCapability {
255    TargetDiscovery,
256    SymbolGraph,
257    GeneratedTests,
258    ExistingTests,
259    PropertyTests,
260    Fuzzing,
261    MutationChecks,
262    Coverage,
263    DifferentialReplay,
264    CorpusReplay,
265    RegressionPromotion,
266    ResourceBudgets,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
270#[serde(rename_all = "snake_case")]
271pub enum ArtifactStatus {
272    Planned,
273    Written,
274    Skipped,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278pub struct TestRunResult {
279    pub language: String,
280    pub status: RunStatus,
281    pub commands: Vec<CommandRecord>,
282    pub failures: Vec<Failure>,
283    pub duration_ms: u128,
284    #[serde(default)]
285    pub quality: VerificationQuality,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289#[serde(rename_all = "snake_case")]
290pub enum RunStatus {
291    Passed,
292    Failed,
293    Skipped,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
297pub struct CommandRecord {
298    pub program: String,
299    pub args: Vec<String>,
300    pub cwd: Utf8PathBuf,
301    pub exit_code: Option<i32>,
302    pub status: RunStatus,
303    pub stdout: String,
304    pub stderr: String,
305    pub duration_ms: u128,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309pub struct Failure {
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub id: Option<String>,
312    pub message: String,
313    #[serde(default = "default_failure_severity")]
314    pub severity: FailureSeverity,
315    pub target_id: Option<String>,
316    pub artifact_id: Option<String>,
317    pub command: String,
318    pub stdout_excerpt: String,
319    pub stderr_excerpt: String,
320    pub repro: Option<ReproCase>,
321}
322
323fn default_failure_severity() -> FailureSeverity {
324    FailureSeverity::Error
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328pub struct ReproCase {
329    pub command: String,
330    pub input: Option<String>,
331    pub path: Option<Utf8PathBuf>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
335pub struct BehaviorReplayCase {
336    pub name: String,
337    pub inputs: Vec<serde_json::Value>,
338    pub assertion: Option<String>,
339    #[serde(default, skip_serializing_if = "is_false")]
340    pub argument_tuple: bool,
341}
342
343#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
344#[serde(rename_all = "snake_case")]
345pub enum BehaviorReplayStatus {
346    Observed,
347    Unsupported,
348    Failed,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
352pub struct BehaviorReplayObservation {
353    pub status: BehaviorReplayStatus,
354    pub output: serde_json::Value,
355    pub command: Option<String>,
356    pub stdout_excerpt: Option<String>,
357    pub stderr_excerpt: Option<String>,
358    pub duration_ms: Option<u128>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
362pub struct AssertionCandidate {
363    pub language: String,
364    pub target_id: String,
365    pub finding_id: Option<String>,
366    pub source: AssertionSource,
367    pub domain: AssertionDomain,
368    #[serde(default, skip_serializing_if = "Vec::is_empty")]
369    pub semantic_packs: Vec<String>,
370    pub title: String,
371    pub seed_inputs: Vec<String>,
372    pub expected_behavior: String,
373    pub replay_command: Option<String>,
374}
375
376#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
377#[serde(rename_all = "snake_case")]
378pub enum AssertionSource {
379    MutationSurvivor,
380    FuzzRepro,
381    GeneratedTestFailure,
382    DifferentialReplay,
383    CoverageGap,
384}
385
386#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
387#[serde(rename_all = "snake_case")]
388pub enum AssertionDomain {
389    AuthPermission,
390    Money,
391    Parsing,
392    Serialization,
393    ErrorHandling,
394    Boundary,
395    General,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399pub struct CorpusEntry {
400    pub language: String,
401    pub target_id: String,
402    pub finding_id: Option<String>,
403    pub source: AssertionSource,
404    pub input: Option<String>,
405    pub path: Option<Utf8PathBuf>,
406    pub replay_command: String,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
410pub struct CommandBudget {
411    pub language: String,
412    pub target_id: String,
413    pub budget_seconds: u64,
414    pub max_concurrency: usize,
415    pub resource_limits: Vec<String>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
419pub struct ConfidenceScore {
420    pub score: u8,
421    pub grade: ConfidenceGrade,
422    pub summary: String,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub correctness_mutation_score_percent: Option<u8>,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub brittleness_probe_survival_percent: Option<u8>,
427    #[serde(default, skip_serializing_if = "is_zero")]
428    pub brittleness_probes_executed: usize,
429    #[serde(default, skip_serializing_if = "is_zero")]
430    pub brittleness_probes_killed: usize,
431    pub positive_signals: Vec<String>,
432    pub risks: Vec<String>,
433    pub recommended_next_steps: Vec<String>,
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub baseline_delta: Option<QualityDelta>,
436}
437
438#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
439#[serde(rename_all = "snake_case")]
440pub enum ConfidenceGrade {
441    Low,
442    Medium,
443    High,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
447pub struct QualityBaseline {
448    pub version: u32,
449    pub quality: VerificationQuality,
450    pub confidence: u8,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454pub struct QualityDelta {
455    pub mutation_score_delta: Option<i16>,
456    pub confidence_delta: i16,
457    pub surviving_mutants_delta: i64,
458    pub corpus_entries_delta: i64,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
462pub struct CoverageReport {
463    pub tool: String,
464    pub summary: String,
465    pub files: Vec<CoverageFile>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
469pub struct CoverageFile {
470    pub path: Utf8PathBuf,
471    pub line_coverage_percent: Option<u8>,
472    pub uncovered_ranges: Vec<String>,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
476pub struct VerificationReport {
477    pub project: Option<ProjectInfo>,
478    pub targets: Vec<VerificationTarget>,
479    pub plan: Option<VerificationPlan>,
480    pub artifacts: Vec<GeneratedArtifact>,
481    pub runs: Vec<TestRunResult>,
482    pub coverage: Vec<CoverageReport>,
483    pub findings: Vec<Failure>,
484    #[serde(default)]
485    pub quality: VerificationQuality,
486    pub suggested_next_steps: Vec<String>,
487}
488
489impl VerificationReport {
490    pub fn empty() -> Self {
491        Self {
492            project: None,
493            targets: Vec::new(),
494            plan: None,
495            artifacts: Vec::new(),
496            runs: Vec::new(),
497            coverage: Vec::new(),
498            findings: Vec::new(),
499            quality: VerificationQuality::default(),
500            suggested_next_steps: Vec::new(),
501        }
502    }
503}
504
505#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
506pub struct VerificationQuality {
507    pub mutation: MutationMetrics,
508    pub property: PropertyMetrics,
509    pub fuzz: FuzzMetrics,
510    #[serde(default)]
511    pub regression: RegressionMetrics,
512    #[serde(default)]
513    pub replay: ReplayMetrics,
514    #[serde(default)]
515    pub budget: BudgetMetrics,
516    #[serde(default)]
517    pub evolution: EvolutionMetrics,
518    #[serde(default)]
519    pub performance: PerformanceMetrics,
520}
521
522#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
523pub struct MutationMetrics {
524    pub generated: usize,
525    #[serde(default)]
526    pub runnable: usize,
527    pub executed: usize,
528    pub killed: usize,
529    pub survived: usize,
530    #[serde(default)]
531    pub not_covered: usize,
532    #[serde(default)]
533    pub timed_out: usize,
534    #[serde(default)]
535    pub not_viable: usize,
536    pub skipped: usize,
537    #[serde(default, skip_serializing_if = "is_zero")]
538    pub requested_workers: usize,
539    #[serde(default, skip_serializing_if = "is_zero")]
540    pub effective_workers: usize,
541    #[serde(default, skip_serializing_if = "is_zero")]
542    pub isolation_failures: usize,
543    #[serde(default, skip_serializing_if = "is_zero_u128")]
544    pub isolation_setup_ms: u128,
545    #[serde(default)]
546    pub isolation_copy_ms: u128,
547    #[serde(default, skip_serializing_if = "is_zero")]
548    pub isolation_excluded_path_count: usize,
549    #[serde(default, skip_serializing_if = "Vec::is_empty")]
550    pub isolation_excluded_path_samples: Vec<Utf8PathBuf>,
551    #[serde(default, skip_serializing_if = "Vec::is_empty")]
552    pub isolation_exclusion_patterns: Vec<String>,
553    #[serde(default, skip_serializing_if = "Vec::is_empty")]
554    pub isolation_runs: Vec<MutationIsolationRecord>,
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub baseline_duration_ms: Option<u128>,
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    pub computed_timeout_seconds: Option<u64>,
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub timeout_source: Option<String>,
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub score_percent: Option<u8>,
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub correctness_score_percent: Option<u8>,
565    #[serde(default, skip_serializing_if = "is_zero")]
566    pub correctness_executed: usize,
567    #[serde(default, skip_serializing_if = "is_zero")]
568    pub correctness_killed: usize,
569    #[serde(default, skip_serializing_if = "is_zero")]
570    pub correctness_survived: usize,
571    #[serde(default, skip_serializing_if = "is_zero")]
572    pub brittleness_executed: usize,
573    #[serde(default, skip_serializing_if = "is_zero")]
574    pub brittleness_killed: usize,
575    #[serde(default, skip_serializing_if = "is_zero")]
576    pub brittleness_survived: usize,
577    #[serde(default, skip_serializing_if = "Option::is_none")]
578    pub brittleness_survival_percent: Option<u8>,
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub efficacy_percent: Option<u8>,
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub mutant_coverage_percent: Option<u8>,
583    #[serde(default)]
584    pub by_domain: BTreeMap<String, MutationAttribution>,
585    #[serde(default)]
586    pub by_operator: BTreeMap<String, MutationAttribution>,
587    #[serde(default, skip_serializing_if = "Vec::is_empty")]
588    pub records: Vec<MutationRecord>,
589}
590
591#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
592pub struct MutationIsolationRecord {
593    pub language: String,
594    pub worker_index: usize,
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub shard_index: Option<usize>,
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub mutant_id: Option<String>,
599    #[serde(default, skip_serializing_if = "Option::is_none")]
600    pub scratch_root: Option<Utf8PathBuf>,
601    #[serde(default)]
602    pub copy_duration_ms: u128,
603    #[serde(default, skip_serializing_if = "is_zero")]
604    pub excluded_path_count: usize,
605    #[serde(default, skip_serializing_if = "Vec::is_empty")]
606    pub excluded_path_samples: Vec<Utf8PathBuf>,
607    #[serde(default, skip_serializing_if = "Vec::is_empty")]
608    pub exclusion_patterns: Vec<String>,
609    #[serde(default, skip_serializing_if = "Option::is_none")]
610    pub error: Option<String>,
611}
612
613#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
614pub struct MutationAttribution {
615    pub generated: usize,
616    #[serde(default)]
617    pub runnable: usize,
618    pub executed: usize,
619    pub killed: usize,
620    pub survived: usize,
621    #[serde(default)]
622    pub not_covered: usize,
623    #[serde(default)]
624    pub timed_out: usize,
625    #[serde(default)]
626    pub not_viable: usize,
627    pub skipped: usize,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
631pub struct MutationRecord {
632    pub id: String,
633    pub language: String,
634    pub path: Utf8PathBuf,
635    pub symbol: String,
636    pub operator: String,
637    pub domain: String,
638    pub status: MutationStatus,
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub from: Option<String>,
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub to: Option<String>,
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub line_range: Option<LineRange>,
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub source_span: Option<SourceSpan>,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub diff: Option<String>,
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub diff_path: Option<Utf8PathBuf>,
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub outcome_path: Option<Utf8PathBuf>,
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub command_log_path: Option<Utf8PathBuf>,
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub stdout_log_path: Option<Utf8PathBuf>,
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub stderr_log_path: Option<Utf8PathBuf>,
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub risk_note: Option<String>,
661    #[serde(default, skip_serializing_if = "Option::is_none")]
662    pub suggested_test: Option<String>,
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub skip_reason: Option<String>,
665    #[serde(default, skip_serializing_if = "Option::is_none")]
666    pub selected_test_command: Option<String>,
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub test_selection_hint: Option<String>,
669    #[serde(default, skip_serializing_if = "Option::is_none")]
670    pub test_selection_fallback: Option<String>,
671    #[serde(default, skip_serializing_if = "is_false")]
672    pub brittleness_probe: bool,
673    #[serde(default, skip_serializing_if = "Option::is_none")]
674    pub command: Option<String>,
675    #[serde(default)]
676    pub duration_ms: u128,
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
680pub struct SourceSpan {
681    pub start_byte: usize,
682    pub end_byte: usize,
683}
684
685pub mod mutation_taxonomy {
686    pub const DOMAINS: &[&str] = &[
687        "auth_permission",
688        "money",
689        "parsing_normalization",
690        "serialization",
691        "error_handling",
692        "boundary",
693        "concurrency_lifecycle",
694        "synchronization",
695        "database",
696        "retry_resilience",
697        "testability",
698        "brittleness",
699        "general",
700    ];
701
702    pub const OPERATORS: &[&str] = &[
703        "comparison",
704        "equality",
705        "boolean",
706        "arithmetic",
707        "bitwise",
708        "assignment",
709        "increment",
710        "loop",
711        "literal",
712        "negation",
713        "default",
714        "nil",
715        "error",
716        "boundary",
717        "await_join",
718        "task_spawn",
719        "context_timeout",
720        "lock_mode",
721        "unlock_guard",
722        "channel_select",
723        "atomic_ordering",
724        "transaction_boundary",
725        "rollback_commit",
726        "isolation_lock",
727        "idempotency",
728        "tenant_filter",
729        "affected_rows",
730        "retry_attempt",
731        "retry_classifier",
732        "backoff_cap",
733        "injected_clock",
734        "injected_randomness",
735        "injected_repository",
736        "test_scheduler",
737        "config_lookup",
738        "temp_isolation",
739        "error_shape",
740        "equivalent_ordering",
741        "log_metric_noise",
742        "private_refactor",
743        "general",
744    ];
745
746    pub fn valid_domain(domain: &str) -> bool {
747        DOMAINS.contains(&domain)
748    }
749
750    pub fn valid_operator(operator: &str) -> bool {
751        OPERATORS.contains(&operator)
752    }
753
754    pub fn normalize_domain(label: &str) -> &'static str {
755        let label = label.to_ascii_lowercase().replace(['/', '-'], "_");
756        for domain in DOMAINS {
757            if label.contains(domain) {
758                return domain;
759            }
760        }
761        if label.contains("permission") || label.contains("auth") || label.contains("token") {
762            "auth_permission"
763        } else if label.contains("parse") || label.contains("format") || label.contains("normal") {
764            "parsing_normalization"
765        } else if label.contains("error") || label.contains("err") || label.contains("nil") {
766            "error_handling"
767        } else if label.contains("await") || label.contains("spawn") || label.contains("task") {
768            "concurrency_lifecycle"
769        } else if label.contains("lock")
770            || label.contains("unlock")
771            || label.contains("channel")
772            || label.contains("select")
773            || label.contains("atomic")
774            || label.contains("ordering")
775        {
776            "synchronization"
777        } else if label.contains("transaction")
778            || label.contains("commit")
779            || label.contains("rollback")
780            || label.contains("isolation")
781            || label.contains("tenant")
782            || label.contains("idempot")
783            || label.contains("rows affected")
784        {
785            "database"
786        } else if label.contains("retry")
787            || label.contains("backoff")
788            || label.contains("transient")
789        {
790            "retry_resilience"
791        } else if label.contains("clock")
792            || label.contains("random")
793            || label.contains("repository")
794            || label.contains("scheduler")
795            || label.contains("config")
796            || label.contains("temp")
797        {
798            "testability"
799        } else if label.contains("equivalent")
800            || label.contains("noise")
801            || label.contains("private")
802            || label.contains("brittle")
803        {
804            "brittleness"
805        } else if label.contains("boundary")
806            || label.contains("limit")
807            || label.contains("threshold")
808            || label.contains("min")
809            || label.contains("max")
810        {
811            "boundary"
812        } else if label.contains("money")
813            || label.contains("price")
814            || label.contains("invoice")
815            || label.contains("total")
816            || label.contains("refund")
817            || label.contains("discount")
818        {
819            "money"
820        } else if label.contains("serial") || label.contains("json") || label.contains("marshal") {
821            "serialization"
822        } else {
823            "general"
824        }
825    }
826
827    pub fn normalize_operator(label: &str) -> &'static str {
828        let label = label.to_ascii_lowercase().replace(['/', '-'], "_");
829        for operator in OPERATORS {
830            if label.contains(operator) {
831                return operator;
832            }
833        }
834        if label.contains("equality") {
835            "equality"
836        } else if label.contains("comparison") {
837            "comparison"
838        } else if label.contains("boolean") || label.contains("connector") {
839            "boolean"
840        } else if label.contains("arithmetic") {
841            "arithmetic"
842        } else if label.contains("bitwise") {
843            "bitwise"
844        } else if label.contains("assignment") {
845            "assignment"
846        } else if label.contains("increment") || label.contains("decrement") {
847            "increment"
848        } else if label.contains("loop") {
849            "loop"
850        } else if label.contains("literal") || label.contains("value perturbation") {
851            "literal"
852        } else if label.contains("negation") {
853            "negation"
854        } else if label.contains("default") {
855            "default"
856        } else if label.contains("nil") {
857            "nil"
858        } else if label.contains("error") {
859            "error"
860        } else if label.contains("boundary") {
861            "boundary"
862        } else {
863            "general"
864        }
865    }
866
867    pub fn risk_note(domain: &str, operator: &str) -> &'static str {
868        match (domain, operator) {
869            ("concurrency_lifecycle", "await_join") => {
870                "Async lifecycle mutation: tests should prove awaited work is observed before returning."
871            }
872            ("concurrency_lifecycle", "task_spawn") => {
873                "Task lifecycle mutation: tests should catch fire-and-forget or missing goroutine behavior."
874            }
875            ("synchronization", "lock_mode") => {
876                "Synchronization mutation: tests should catch read/write lock or critical-section weakening."
877            }
878            ("synchronization", "unlock_guard") => {
879                "Synchronization mutation: tests should detect lock release timing and cleanup guarantees."
880            }
881            ("synchronization", "channel_select") => {
882                "Channel/select mutation: tests should cover cancellation, timeout, and chosen receive/send paths."
883            }
884            ("synchronization", "atomic_ordering") => {
885                "Atomic ordering mutation: tests should exercise concurrent visibility assumptions."
886            }
887            ("database", "transaction_boundary") | ("database", "rollback_commit") => {
888                "Database mutation: tests should assert transaction commit/rollback behavior and failure atomicity."
889            }
890            ("database", "isolation_lock") => {
891                "Database isolation mutation: tests should catch lost locking or isolation guarantees."
892            }
893            ("database", "tenant_filter") => {
894                "Tenant isolation mutation: tests should prove cross-tenant data cannot leak."
895            }
896            ("database", "idempotency") => {
897                "Idempotency mutation: tests should replay duplicate requests and assert stable side effects."
898            }
899            ("retry_resilience", _) => {
900                "Retry/resilience mutation: tests should cover transient failures, retry limits, and backoff choices."
901            }
902            ("testability", _) => {
903                "Testing seam mutation: this code may be brittle without injected time, randomness, IO, or schedulers."
904            }
905            ("brittleness", _) => {
906                "Brittleness probe: killed behavior-preserving mutants point to tests coupled to ordering, logs, formatting, or implementation noise."
907            }
908            _ => "Mutation should be killed by behavior-focused tests over the affected symbol.",
909        }
910    }
911
912    pub fn suggested_test(domain: &str, operator: &str) -> &'static str {
913        match (domain, operator) {
914            ("concurrency_lifecycle", _) => {
915                "Add a deterministic concurrent test that waits for completion and asserts observable side effects."
916            }
917            ("synchronization", "channel_select") => {
918                "Add channel/select tests for ready, blocked, cancellation, and timeout paths."
919            }
920            ("synchronization", _) => {
921                "Add a contention test with deterministic scheduling or repeated stress under the focused symbol."
922            }
923            ("database", "tenant_filter") => {
924                "Add a cross-tenant fixture and assert each query/update is scoped to the active tenant."
925            }
926            ("database", _) => {
927                "Add transaction tests that force success and failure paths and inspect persisted state."
928            }
929            ("retry_resilience", _) => {
930                "Use a fake dependency that fails transiently, then assert retry count, backoff cap, and final result."
931            }
932            ("testability", _) => {
933                "Introduce an injectable seam for time/randomness/IO and assert deterministic behavior through it."
934            }
935            ("brittleness", _) => {
936                "If this probe was killed, loosen exact implementation assertions into behavior assertions unless the detail is contractual."
937            }
938            _ => "Add a regression assertion that distinguishes the original expression from this mutant.",
939        }
940    }
941}
942
943fn is_zero(value: &usize) -> bool {
944    *value == 0
945}
946
947fn is_zero_u128(value: &u128) -> bool {
948    *value == 0
949}
950
951pub fn finalize_mutation_metrics(mutation: &mut MutationMetrics) {
952    mutation.skipped = mutation.generated.saturating_sub(mutation.executed);
953    mutation.score_percent = percent(mutation.killed, mutation.executed);
954    mutation.efficacy_percent = percent(mutation.killed, mutation.killed + mutation.survived);
955    mutation.mutant_coverage_percent = percent(
956        mutation.killed + mutation.survived,
957        mutation.killed + mutation.survived + mutation.not_covered,
958    );
959
960    let brittleness = mutation
961        .by_domain
962        .get("brittleness")
963        .cloned()
964        .unwrap_or_default();
965    mutation.brittleness_executed = brittleness.executed;
966    mutation.brittleness_killed = brittleness.killed;
967    mutation.brittleness_survived = brittleness.survived;
968    mutation.brittleness_survival_percent =
969        percent(mutation.brittleness_survived, mutation.brittleness_executed);
970
971    mutation.correctness_executed = mutation
972        .executed
973        .saturating_sub(mutation.brittleness_executed);
974    mutation.correctness_killed = mutation.killed.saturating_sub(mutation.brittleness_killed);
975    mutation.correctness_survived = mutation
976        .survived
977        .saturating_sub(mutation.brittleness_survived);
978    mutation.correctness_score_percent =
979        percent(mutation.correctness_killed, mutation.correctness_executed);
980}
981
982fn percent(numerator: usize, denominator: usize) -> Option<u8> {
983    (numerator * 100)
984        .checked_div(denominator)
985        .map(|score| score.try_into().unwrap_or(100))
986}
987
988fn is_false(value: &bool) -> bool {
989    !*value
990}
991
992#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
993#[serde(rename_all = "snake_case")]
994pub enum MutationStatus {
995    Runnable,
996    NotCovered,
997    Killed,
998    Lived,
999    TimedOut,
1000    NotViable,
1001    Skipped,
1002}
1003
1004#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1005pub struct PropertyMetrics {
1006    pub generated_artifacts: usize,
1007    pub failed_generated_tests: usize,
1008    pub no_panic_properties: usize,
1009    pub deterministic_properties: usize,
1010    pub invariant_properties: usize,
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    pub strength_score_percent: Option<u8>,
1013}
1014
1015#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1016pub struct FuzzMetrics {
1017    pub generated_harnesses: usize,
1018    pub targets_executed: usize,
1019    pub failures: usize,
1020    pub persisted_repros: usize,
1021}
1022
1023#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1024pub struct RegressionMetrics {
1025    pub assertion_candidates: usize,
1026    pub promoted_scaffolds: usize,
1027    pub corpus_entries: usize,
1028    pub corpus_replayed: usize,
1029    pub corpus_passed: usize,
1030    pub corpus_failed: usize,
1031    pub corpus_skipped: usize,
1032}
1033
1034#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1035pub struct ReplayMetrics {
1036    pub manifests: usize,
1037    pub targets: usize,
1038    pub cases: usize,
1039    pub results: usize,
1040}
1041
1042#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1043pub struct BudgetMetrics {
1044    pub budget_plans: usize,
1045    pub skipped_commands: usize,
1046    pub timed_out_commands: usize,
1047}
1048
1049#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1050pub struct EvolutionMetrics {
1051    pub suites: usize,
1052    pub candidates: usize,
1053    pub selected: usize,
1054    pub property_candidates: usize,
1055    pub mutation_candidates: usize,
1056    pub fuzz_candidates: usize,
1057    pub regression_candidates: usize,
1058    pub replay_candidates: usize,
1059    #[serde(skip_serializing_if = "Option::is_none")]
1060    pub average_fitness_percent: Option<u8>,
1061}
1062
1063#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1064pub struct PerformanceMetrics {
1065    #[serde(default, skip_serializing_if = "is_zero_u128")]
1066    pub discovery_ms: u128,
1067    #[serde(default, skip_serializing_if = "is_zero_u128")]
1068    pub generation_ms: u128,
1069    #[serde(default, skip_serializing_if = "is_zero_u128")]
1070    pub test_execution_ms: u128,
1071    #[serde(default, skip_serializing_if = "is_zero_u128")]
1072    pub coverage_ms: u128,
1073    #[serde(default, skip_serializing_if = "is_zero_u128")]
1074    pub replay_ms: u128,
1075    #[serde(default, skip_serializing_if = "is_zero_u128")]
1076    pub artifact_synthesis_ms: u128,
1077    #[serde(default, skip_serializing_if = "is_zero_u128")]
1078    pub total_ms: u128,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1082pub struct EvolutionSuite {
1083    pub version: u8,
1084    pub language: String,
1085    pub generation: u32,
1086    pub selection_budget: usize,
1087    pub fitness_signals: Vec<String>,
1088    pub candidates: Vec<EvolutionCandidateRecord>,
1089}
1090
1091#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1092pub struct EvolutionCandidateRecord {
1093    pub id: String,
1094    pub language: String,
1095    pub target_id: String,
1096    pub kind: EvolutionCandidateKind,
1097    pub strategy: EvolutionStrategy,
1098    pub status: EvolutionCandidateStatus,
1099    #[serde(default, skip_serializing_if = "Option::is_none")]
1100    pub source_artifact: Option<Utf8PathBuf>,
1101    #[serde(default, skip_serializing_if = "Option::is_none")]
1102    pub source_finding_id: Option<String>,
1103    #[serde(default, skip_serializing_if = "Option::is_none")]
1104    pub domain: Option<String>,
1105    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1106    pub semantic_packs: Vec<String>,
1107    pub fitness: EvolutionFitness,
1108    pub proposed_action: String,
1109    pub keep_if: String,
1110    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1111    pub proof_commands: Vec<String>,
1112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1113    pub done_when: Vec<String>,
1114}
1115
1116#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1117#[serde(rename_all = "snake_case")]
1118pub enum EvolutionCandidateKind {
1119    Property,
1120    Mutation,
1121    Fuzz,
1122    Regression,
1123    Replay,
1124    Budget,
1125}
1126
1127#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1128#[serde(rename_all = "snake_case")]
1129pub enum EvolutionStrategy {
1130    AddAssertion,
1131    StrengthenProperty,
1132    PromoteCorpus,
1133    AddFuzzSeed,
1134    NarrowTarget,
1135    ReduceBudgetRisk,
1136}
1137
1138#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1139#[serde(rename_all = "snake_case")]
1140pub enum EvolutionCandidateStatus {
1141    Proposed,
1142    Selected,
1143    Applied,
1144    Kept,
1145    Superseded,
1146    Rejected,
1147}
1148
1149#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1150pub struct EvolutionFitness {
1151    pub score_percent: u8,
1152    pub mutation_delta: i16,
1153    pub finding_delta: i16,
1154    pub replay_delta: i16,
1155    pub confidence_delta: i16,
1156    pub rationale: String,
1157}
1158
1159#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1160#[serde(rename_all = "snake_case")]
1161pub enum EvolutionOutcome {
1162    Improved,
1163    Neutral,
1164    Regressed,
1165    FailedToEvaluate,
1166}
1167
1168#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1169pub struct EvolutionQualityDelta {
1170    #[serde(skip_serializing_if = "Option::is_none")]
1171    pub mutation_score_delta: Option<i16>,
1172    pub killed_mutants_delta: i64,
1173    pub surviving_mutants_delta: i64,
1174    pub not_covered_mutants_delta: i64,
1175    pub findings_delta: i64,
1176    pub replay_cases_delta: i64,
1177    pub corpus_failed_delta: i64,
1178    pub budget_timed_out_delta: i64,
1179    pub budget_skipped_delta: i64,
1180    pub confidence_delta: i16,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1184pub struct EvolutionGeneration {
1185    pub version: u8,
1186    pub language: String,
1187    pub generation: u32,
1188    pub parent_suite: Utf8PathBuf,
1189    pub created_unix_seconds: u64,
1190    pub outcome: EvolutionOutcome,
1191    pub candidates: Vec<EvolutionGenerationCandidate>,
1192    #[serde(default, skip_serializing_if = "Option::is_none")]
1193    pub delta: Option<EvolutionQualityDelta>,
1194    pub written_paths: Vec<Utf8PathBuf>,
1195}
1196
1197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1198pub struct EvolutionGenerationCandidate {
1199    pub id: String,
1200    pub target_id: String,
1201    pub previous_status: EvolutionCandidateStatus,
1202    pub status: EvolutionCandidateStatus,
1203    pub outcome: EvolutionOutcome,
1204    pub applied: bool,
1205    pub written_paths: Vec<Utf8PathBuf>,
1206    #[serde(default, skip_serializing_if = "Option::is_none")]
1207    pub skipped_reason: Option<String>,
1208}