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}