Skip to main content

asupersync/lab/crashpack/
mod.rs

1//! ATP transfer oracles and crashpack infrastructure.
2//!
3//! This module implements ATP-L2 requirements for deterministic failure reproduction:
4//!
5//! - **Transfer oracles**: Validate manifest integrity, journal consistency,
6//!   quiescence, obligation leaks, path outcome consistency
7//! - **Crashpacks**: Serializable failure artifacts for reproduction
8//! - **Evidence ledger**: Record seeds, oracle failures, and artifact paths
9//! - **Replay coordination**: Bridge lab models to deterministic replay
10//!
11//! # Quick Start
12//!
13//! ```ignore
14//! use asupersync::lab::crashpack::{TransferOracle, CrashpackBuilder};
15//!
16//! // Create oracle for transfer validation
17//! let oracle = TransferOracle::new("manifest_integrity");
18//! let result = oracle.validate_transfer(&transfer_state);
19//!
20//! // Build crashpack on failure
21//! if result.has_violations() {
22//!     let crashpack = CrashpackBuilder::new()
23//!         .with_oracle_result(result)
24//!         .with_trace(&trace_buffer)
25//!         .build()?;
26//!
27//!     crashpack.emit_atp_trace("failure_artifacts/")?;
28//! }
29//! ```
30
31pub mod agent_proof;
32pub mod evidence_ledger;
33pub mod oracle;
34pub mod replay;
35
36// Re-export key types for convenience
37pub use agent_proof::{
38    AgentProofError, AgentTaskProofBundle, AgentTaskProofBundleBuilder, BlockerRecord,
39    CommandRecord, CommitRecord, FileReservationRecord, RchRecord, ReplayInstructions,
40    ReplaySafetyLevel, ValidationFrontierRecord,
41};
42pub use evidence_ledger::{AtpEvidenceEntry, AtpEvidenceLedger, EvidenceSummary};
43pub use oracle::{AtpOracleChecks, AtpOracleResult, AtpTransferOracle, AtpTransferState};
44pub use replay::{
45    AtpReplayCoordinator, AtpReplayResult, ReplayError, TraceMinimizer, TraceMinimizerConfig,
46};
47
48use crate::lab::oracle::OracleStats;
49use crate::lab::oracle::evidence::{
50    BayesFactor, EvidenceEntry, EvidenceLine, EvidenceStrength, LogLikelihoodContributions,
51};
52use crate::trace::{TraceBuffer, TraceData, TraceEvent, TraceEventKind};
53use serde::{Deserialize, Serialize};
54use sha2::{Digest, Sha256};
55use std::collections::BTreeMap;
56use std::path::{Path, PathBuf};
57use thiserror::Error;
58
59/// ATP crashpack schema version for serialization compatibility.
60pub const ATP_CRASHPACK_SCHEMA_VERSION: u32 = 1;
61
62/// Transfer oracle for ATP-specific validation checks.
63#[derive(Debug, Clone)]
64pub struct TransferOracle {
65    oracle_name: String,
66    manifest_checks: bool,
67    journal_checks: bool,
68    quiescence_checks: bool,
69    obligation_checks: bool,
70    path_consistency_checks: bool,
71}
72
73impl TransferOracle {
74    /// Create a new transfer oracle with the given name.
75    pub fn new(name: impl Into<String>) -> Self {
76        Self {
77            oracle_name: name.into(),
78            manifest_checks: true,
79            journal_checks: true,
80            quiescence_checks: true,
81            obligation_checks: true,
82            path_consistency_checks: true,
83        }
84    }
85
86    /// Enable/disable manifest integrity checks.
87    pub fn with_manifest_checks(mut self, enabled: bool) -> Self {
88        self.manifest_checks = enabled;
89        self
90    }
91
92    /// Enable/disable journal consistency checks.
93    pub fn with_journal_checks(mut self, enabled: bool) -> Self {
94        self.journal_checks = enabled;
95        self
96    }
97
98    /// Validate a transfer operation with configured checks.
99    pub fn validate_transfer(&self, state: &TransferState) -> TransferOracleResult {
100        let mut violations = Vec::new();
101        let mut stats = OracleStats {
102            entities_tracked: 0,
103            events_recorded: 0,
104        };
105
106        if self.manifest_checks {
107            if let Some(violation) = self.check_manifest_integrity(state) {
108                violations.push(violation);
109                stats.entities_tracked += 1;
110            }
111            stats.events_recorded += 1;
112        }
113
114        if self.journal_checks {
115            if let Some(violation) = self.check_journal_consistency(state) {
116                violations.push(violation);
117                stats.entities_tracked += 1;
118            }
119            stats.events_recorded += 1;
120        }
121
122        if self.quiescence_checks {
123            if let Some(violation) = self.check_quiescence(state) {
124                violations.push(violation);
125                stats.entities_tracked += 1;
126            }
127            stats.events_recorded += 1;
128        }
129
130        if self.obligation_checks {
131            if let Some(violation) = self.check_obligation_leaks(state) {
132                violations.push(violation);
133                stats.entities_tracked += 1;
134            }
135            stats.events_recorded += 1;
136        }
137
138        if self.path_consistency_checks {
139            if let Some(violation) = self.check_path_consistency(state) {
140                violations.push(violation);
141                stats.entities_tracked += 1;
142            }
143            stats.events_recorded += 1;
144        }
145
146        let passed = stats.entities_tracked == 0;
147        TransferOracleResult {
148            oracle_name: self.oracle_name.clone(),
149            violations,
150            stats,
151            passed,
152        }
153    }
154
155    fn check_manifest_integrity(&self, state: &TransferState) -> Option<TransferViolation> {
156        // Check that manifest hash matches expected
157        if state.manifest_hash != state.expected_manifest_hash {
158            return Some(TransferViolation {
159                violation_type: "manifest_integrity".to_string(),
160                description: format!(
161                    "Manifest hash mismatch: expected {}, got {}",
162                    state.expected_manifest_hash, state.manifest_hash
163                ),
164                severity: ViolationSeverity::High,
165                evidence: BTreeMap::from([
166                    (
167                        "expected_hash".to_string(),
168                        state.expected_manifest_hash.clone(),
169                    ),
170                    ("actual_hash".to_string(), state.manifest_hash.clone()),
171                ]),
172            });
173        }
174        None
175    }
176
177    fn check_journal_consistency(&self, state: &TransferState) -> Option<TransferViolation> {
178        // Check journal entry ordering and completeness
179        if state.journal_gaps > 0 {
180            return Some(TransferViolation {
181                violation_type: "journal_consistency".to_string(),
182                description: format!(
183                    "Journal has {} gaps or ordering violations",
184                    state.journal_gaps
185                ),
186                severity: ViolationSeverity::High,
187                evidence: BTreeMap::from([(
188                    "gap_count".to_string(),
189                    state.journal_gaps.to_string(),
190                )]),
191            });
192        }
193        None
194    }
195
196    fn check_quiescence(&self, state: &TransferState) -> Option<TransferViolation> {
197        // Ensure no pending operations during transfer
198        if state.pending_operations > 0 {
199            return Some(TransferViolation {
200                violation_type: "quiescence".to_string(),
201                description: format!(
202                    "Transfer attempted with {} pending operations",
203                    state.pending_operations
204                ),
205                severity: ViolationSeverity::Medium,
206                evidence: BTreeMap::from([(
207                    "pending_count".to_string(),
208                    state.pending_operations.to_string(),
209                )]),
210            });
211        }
212        None
213    }
214
215    fn check_obligation_leaks(&self, state: &TransferState) -> Option<TransferViolation> {
216        // Check for leaked obligations that should have been cleaned up
217        if state.leaked_obligations > 0 {
218            return Some(TransferViolation {
219                violation_type: "obligation_leak".to_string(),
220                description: format!("Found {} leaked obligations", state.leaked_obligations),
221                severity: ViolationSeverity::High,
222                evidence: BTreeMap::from([(
223                    "leak_count".to_string(),
224                    state.leaked_obligations.to_string(),
225                )]),
226            });
227        }
228        None
229    }
230
231    fn check_path_consistency(&self, state: &TransferState) -> Option<TransferViolation> {
232        // Validate path outcome consistency
233        if !state.path_outcomes_consistent {
234            return Some(TransferViolation {
235                violation_type: "path_consistency".to_string(),
236                description: "Path outcomes are inconsistent across replicas".to_string(),
237                severity: ViolationSeverity::High,
238                evidence: BTreeMap::new(),
239            });
240        }
241        None
242    }
243}
244
245/// State snapshot for transfer oracle validation.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct TransferState {
248    pub manifest_hash: String,
249    pub expected_manifest_hash: String,
250    pub journal_gaps: u32,
251    pub pending_operations: u32,
252    pub leaked_obligations: u32,
253    pub path_outcomes_consistent: bool,
254}
255
256impl TransferState {
257    pub fn new() -> Self {
258        Self {
259            manifest_hash: String::new(),
260            expected_manifest_hash: String::new(),
261            journal_gaps: 0,
262            pending_operations: 0,
263            leaked_obligations: 0,
264            path_outcomes_consistent: true,
265        }
266    }
267}
268
269impl Default for TransferState {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275/// Result of transfer oracle validation.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct TransferOracleResult {
278    pub oracle_name: String,
279    pub violations: Vec<TransferViolation>,
280    pub stats: OracleStats,
281    pub passed: bool,
282}
283
284impl TransferOracleResult {
285    pub fn has_violations(&self) -> bool {
286        !self.violations.is_empty()
287    }
288
289    pub fn high_severity_violations(&self) -> Vec<&TransferViolation> {
290        self.violations
291            .iter()
292            .filter(|v| matches!(v.severity, ViolationSeverity::High))
293            .collect()
294    }
295}
296
297/// Specific violation found by transfer oracle.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct TransferViolation {
300    pub violation_type: String,
301    pub description: String,
302    pub severity: ViolationSeverity,
303    pub evidence: BTreeMap<String, String>,
304}
305
306/// Severity classification for violations.
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308pub enum ViolationSeverity {
309    Low,
310    Medium,
311    High,
312    Critical,
313}
314
315/// Builder for ATP crashpacks containing failure artifacts.
316#[derive(Debug, Default)]
317pub struct CrashpackBuilder {
318    oracle_results: Vec<TransferOracleResult>,
319    trace_buffer: Option<TraceBuffer>,
320    seeds: BTreeMap<String, u64>,
321    artifact_paths: Vec<String>,
322    metadata: BTreeMap<String, String>,
323}
324
325impl CrashpackBuilder {
326    pub fn new() -> Self {
327        Self::default()
328    }
329
330    pub fn with_oracle_result(mut self, result: TransferOracleResult) -> Self {
331        self.oracle_results.push(result);
332        self
333    }
334
335    pub fn with_trace(mut self, trace: TraceBuffer) -> Self {
336        self.trace_buffer = Some(trace);
337        self
338    }
339
340    pub fn with_seed(mut self, name: impl Into<String>, seed: u64) -> Self {
341        self.seeds.insert(name.into(), seed);
342        self
343    }
344
345    pub fn with_artifact_path(mut self, path: impl Into<String>) -> Self {
346        let path = path.into();
347        if !self.artifact_paths.contains(&path) {
348            self.artifact_paths.push(path); // ubs:ignore - pushing to vector, not path join
349        }
350        self
351    }
352
353    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
354        self.metadata.insert(key.into(), value.into());
355        self
356    }
357
358    pub fn build(self) -> Result<AtpCrashpack, CrashpackError> {
359        Ok(AtpCrashpack {
360            schema_version: ATP_CRASHPACK_SCHEMA_VERSION,
361            oracle_results: self.oracle_results,
362            trace_events: self
363                .trace_buffer
364                .as_ref()
365                .map(|buf| buf.iter().cloned().collect())
366                .unwrap_or_default(),
367            seeds: self.seeds,
368            artifact_paths: self.artifact_paths,
369            metadata: self.metadata,
370        })
371    }
372}
373
374/// Serializable crashpack containing all failure reproduction artifacts.
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct AtpCrashpack {
377    pub schema_version: u32,
378    pub oracle_results: Vec<TransferOracleResult>,
379    pub trace_events: Vec<TraceEvent>,
380    pub seeds: BTreeMap<String, u64>,
381    pub artifact_paths: Vec<String>,
382    pub metadata: BTreeMap<String, String>,
383}
384
385impl AtpCrashpack {
386    /// Emit ATP trace artifacts to the specified directory.
387    pub fn emit_atp_trace(&self, output_dir: impl AsRef<Path>) -> Result<(), CrashpackError> {
388        let output_dir = output_dir.as_ref();
389        std::fs::create_dir_all(output_dir)?;
390
391        // Emit transfer.atp-trace
392        let trace_path = output_dir.join("transfer.atp-trace");
393        let trace_data = serde_json::to_string_pretty(&self.trace_events)?;
394        std::fs::write(&trace_path, trace_data)?;
395
396        // Emit journal digest
397        let journal_data = self.generate_journal_digest()?;
398        let journal_digest = journal_digest_ref(&journal_data);
399        let journal_path = output_dir.join("journal");
400        std::fs::write(&journal_path, &journal_data)?;
401
402        let journal_digest_path = output_dir.join("journal.digest");
403        std::fs::write(
404            &journal_digest_path,
405            format!("digest: {journal_digest}\nbytes: {}\n", journal_data.len()),
406        )?;
407
408        // Emit deterministic evidence ledger before the manifest so the
409        // manifest can point at the exact ledger artifact.
410        let evidence_ledger_path = output_dir.join("evidence-ledger.json");
411        let evidence_ledger = self.generate_evidence_ledger();
412        std::fs::write(&evidence_ledger_path, evidence_ledger.export_json()?)?;
413
414        // Emit manifest after the journal so it can point at the exact digest.
415        let manifest_path = output_dir.join("manifest");
416        let manifest_data = self.generate_manifest(&journal_digest)?;
417        std::fs::write(&manifest_path, manifest_data)?;
418
419        // Emit pathlog, quiclog, repairlog
420        self.emit_specialized_logs(output_dir)?;
421
422        // Generate replay command
423        let replay_cmd = self.generate_replay_command()?;
424        let replay_path = output_dir.join("replay_command.sh");
425        std::fs::write(&replay_path, replay_cmd)?;
426
427        Ok(())
428    }
429
430    fn generate_manifest(&self, journal_digest: &str) -> Result<String, CrashpackError> {
431        let mut manifest = format!(
432            "# ATP Crashpack Manifest\nschema_version: {}\nviolations: {}\njournal_digest: {journal_digest}\njournal_digest_artifact: journal.digest\nevidence_ledger: evidence-ledger.json\n",
433            self.schema_version,
434            self.oracle_results
435                .iter()
436                .map(|r| r.violations.len())
437                .sum::<usize>()
438        );
439
440        for (key, value) in &self.metadata {
441            manifest.push_str(&format!("metadata.{key}: {value}\n"));
442        }
443
444        if !self.seeds.is_empty() {
445            manifest.push_str("seeds:\n");
446            for (name, seed) in &self.seeds {
447                manifest.push_str(&format!("  {name}: {seed}\n"));
448            }
449        }
450
451        if !self.artifact_paths.is_empty() {
452            manifest.push_str("artifact_paths:\n");
453            for path in &self.artifact_paths {
454                manifest.push_str(&format!("  - {path}\n"));
455            }
456        }
457
458        Ok(manifest)
459    }
460
461    fn generate_journal_digest(&self) -> Result<String, CrashpackError> {
462        let mut journal = String::from("# ATP Journal Digest\n");
463
464        for result in &self.oracle_results {
465            journal.push_str(&format!("oracle: {}\n", result.oracle_name));
466            journal.push_str(&format!(
467                "  events_recorded: {}\n",
468                result.stats.events_recorded
469            ));
470            journal.push_str(&format!(
471                "  entities_tracked: {}\n",
472                result.stats.entities_tracked
473            ));
474            journal.push_str(&format!("  passed: {}\n", result.passed));
475            if !result.violations.is_empty() {
476                journal.push_str("  violations:\n");
477                for violation in &result.violations {
478                    journal.push_str(&format!("    - type: {}\n", violation.violation_type));
479                    journal.push_str(&format!("      severity: {:?}\n", violation.severity));
480                    journal.push_str(&format!("      description: {}\n", violation.description));
481                    if !violation.evidence.is_empty() {
482                        journal.push_str("      evidence:\n");
483                        for (key, value) in &violation.evidence {
484                            journal.push_str(&format!("        {key}: {value}\n"));
485                        }
486                    }
487                }
488            }
489        }
490
491        Ok(journal)
492    }
493
494    fn generate_evidence_ledger(&self) -> AtpEvidenceLedger {
495        let mut ledger = AtpEvidenceLedger::new();
496
497        for (name, seed) in &self.seeds {
498            ledger.record_seed(name.clone(), *seed);
499        }
500
501        for (key, value) in &self.metadata {
502            ledger.add_metadata(key.clone(), value.clone());
503        }
504
505        for artifact in [
506            "transfer.atp-trace",
507            "manifest",
508            "journal",
509            "journal.digest",
510            "evidence-ledger.json",
511            "pathlog",
512            "quiclog",
513            "repairlog",
514            "replay_command.sh",
515        ] {
516            ledger.record_artifact_path(PathBuf::from(artifact));
517        }
518
519        for artifact in &self.artifact_paths {
520            ledger.record_artifact_path(PathBuf::from(artifact));
521        }
522
523        for result in &self.oracle_results {
524            ledger.record_oracle_result(
525                result.oracle_name.clone(),
526                evidence_for_oracle_result(result),
527                Some(PathBuf::from("transfer.atp-trace")),
528            );
529        }
530
531        ledger
532    }
533
534    fn emit_specialized_logs(&self, output_dir: &Path) -> Result<(), CrashpackError> {
535        self.write_specialized_log(output_dir, "pathlog", is_atp_path_event)?;
536        self.write_specialized_log(output_dir, "quiclog", is_atp_quic_event)?;
537        self.write_specialized_log(output_dir, "repairlog", is_atp_repair_event)?;
538
539        Ok(())
540    }
541
542    fn write_specialized_log(
543        &self,
544        output_dir: &Path,
545        file_name: &str,
546        include: impl Fn(&TraceEvent) -> bool,
547    ) -> Result<(), CrashpackError> {
548        let log = atp_specialized_log(&self.trace_events, include);
549        std::fs::write(output_dir.join(file_name), log)?;
550        Ok(())
551    }
552}
553
554pub(crate) fn atp_specialized_log(
555    trace_events: &[TraceEvent],
556    include: impl Fn(&TraceEvent) -> bool,
557) -> String {
558    trace_events
559        .iter()
560        .filter(|event| include(event))
561        .map(ToString::to_string)
562        .collect::<Vec<_>>()
563        .join("\n")
564}
565
566pub(crate) fn is_atp_path_event(event: &TraceEvent) -> bool {
567    matches!(
568        event.kind,
569        TraceEventKind::Spawn
570            | TraceEventKind::Schedule
571            | TraceEventKind::Yield
572            | TraceEventKind::Wake
573            | TraceEventKind::Poll
574            | TraceEventKind::Complete
575            | TraceEventKind::RegionCreated
576            | TraceEventKind::RegionCloseBegin
577            | TraceEventKind::RegionCloseComplete
578            | TraceEventKind::RegionCancelled
579            | TraceEventKind::Checkpoint
580    ) || message_contains_any(event, &["path", "route", "racing"])
581}
582
583pub(crate) fn is_atp_quic_event(event: &TraceEvent) -> bool {
584    message_contains_any(event, &["quic", "udp", "packet", "connection id"])
585}
586
587pub(crate) fn is_atp_repair_event(event: &TraceEvent) -> bool {
588    message_contains_any(event, &["repair", "raptorq", "fec", "symbol"])
589}
590
591fn message_contains_any(event: &TraceEvent, needles: &[&str]) -> bool {
592    let TraceData::Message(message) = &event.data else {
593        return false;
594    };
595    let message = message.to_ascii_lowercase();
596    needles.iter().any(|needle| message.contains(needle))
597}
598
599impl AtpCrashpack {
600    fn generate_replay_command(&self) -> Result<String, CrashpackError> {
601        let mut cmd = String::from("#!/bin/bash\n");
602        cmd.push_str("# ATP Replay Command\n");
603        cmd.push_str("# Generated by ATP crashpack\n\n");
604
605        // Add seed information
606        for (name, seed) in &self.seeds {
607            cmd.push_str(&format!(
608                "export ATP_SEED_{}={}\n",
609                seed_env_suffix(name),
610                seed
611            ));
612        }
613
614        cmd.push_str("\n# Replay command\n");
615        cmd.push_str(
616            "asupersync atp replay --trace-file transfer.atp-trace --manifest manifest \
617             --journal-digest journal.digest --evidence-ledger evidence-ledger.json \
618             --pathlog pathlog --quiclog quiclog --repairlog repairlog --validate-oracles",
619        );
620
621        // Add oracle flags
622        for result in &self.oracle_results {
623            cmd.push_str(&format!(" --oracle {}", shell_arg(&result.oracle_name)));
624        }
625
626        cmd.push('\n');
627        Ok(cmd)
628    }
629}
630
631fn seed_env_suffix(name: &str) -> String {
632    let mut suffix = String::with_capacity(name.len());
633    for ch in name.chars() {
634        if ch.is_ascii_alphanumeric() {
635            suffix.push(ch.to_ascii_uppercase());
636        } else {
637            suffix.push('_');
638        }
639    }
640
641    let suffix = suffix.trim_matches('_');
642    if suffix.is_empty() {
643        "SEED".to_string()
644    } else {
645        suffix.to_string()
646    }
647}
648
649fn shell_arg(raw: &str) -> String {
650    if !raw.is_empty() && raw.bytes().all(shell_safe_byte) {
651        return raw.to_string();
652    }
653
654    let mut quoted = String::with_capacity(raw.len() + 2);
655    quoted.push('\'');
656    for ch in raw.chars() {
657        if ch == '\'' {
658            quoted.push_str("'\"'\"'");
659        } else {
660            quoted.push(ch);
661        }
662    }
663    quoted.push('\'');
664    quoted
665}
666
667fn shell_safe_byte(byte: u8) -> bool {
668    byte.is_ascii_alphanumeric()
669        || matches!(
670            byte,
671            b'_' | b'-' | b'.' | b'/' | b':' | b'@' | b'%' | b'+' | b'=' | b','
672        )
673}
674
675fn journal_digest_ref(journal_data: &str) -> String {
676    let digest = Sha256::digest(journal_data.as_bytes());
677    format!("sha256:{}", hex::encode(digest))
678}
679
680fn evidence_for_oracle_result(result: &TransferOracleResult) -> EvidenceEntry {
681    let log10_bf = if result.passed {
682        -1.0
683    } else {
684        result
685            .violations
686            .iter()
687            .map(|violation| severity_log10_bf(&violation.severity))
688            .reduce(f64::max)
689            .unwrap_or(0.5)
690    };
691
692    let strength = EvidenceStrength::from_log10_bf(log10_bf);
693    let max_severity = result
694        .violations
695        .iter()
696        .max_by_key(|violation| severity_rank(&violation.severity))
697        .map_or("none", |violation| severity_label(&violation.severity));
698
699    EvidenceEntry {
700        invariant: result.oracle_name.clone(),
701        passed: result.passed,
702        bayes_factor: BayesFactor {
703            log10_bf,
704            hypothesis: format!("{} violation", result.oracle_name),
705            strength,
706        },
707        log_likelihoods: LogLikelihoodContributions {
708            structural: log10_bf / 2.0,
709            detection: log10_bf / 2.0,
710            total: log10_bf,
711        },
712        evidence_lines: vec![EvidenceLine {
713            equation: "BF = P(oracle evidence | violation) / P(oracle evidence | clean)"
714                .to_string(),
715            substitution: format!(
716                "passed={}, violations={}, events_recorded={}, entities_tracked={}, max_severity={max_severity}",
717                result.passed,
718                result.violations.len(),
719                result.stats.events_recorded,
720                result.stats.entities_tracked
721            ),
722            intuition: if result.passed {
723                format!(
724                    "{} produced deterministic clean evidence",
725                    result.oracle_name
726                )
727            } else {
728                format!(
729                    "{} reported {} deterministic violation(s)",
730                    result.oracle_name,
731                    result.violations.len()
732                )
733            },
734        }],
735    }
736}
737
738fn severity_log10_bf(severity: &ViolationSeverity) -> f64 {
739    match severity {
740        ViolationSeverity::Low => 0.6,
741        ViolationSeverity::Medium => 1.0,
742        ViolationSeverity::High => 1.6,
743        ViolationSeverity::Critical => 2.4,
744    }
745}
746
747fn severity_label(severity: &ViolationSeverity) -> &'static str {
748    match severity {
749        ViolationSeverity::Low => "low",
750        ViolationSeverity::Medium => "medium",
751        ViolationSeverity::High => "high",
752        ViolationSeverity::Critical => "critical",
753    }
754}
755
756fn severity_rank(severity: &ViolationSeverity) -> u8 {
757    match severity {
758        ViolationSeverity::Low => 0,
759        ViolationSeverity::Medium => 1,
760        ViolationSeverity::High => 2,
761        ViolationSeverity::Critical => 3,
762    }
763}
764
765/// Errors during crashpack operations.
766#[derive(Debug, Error)]
767pub enum CrashpackError {
768    #[error("IO error: {0}")]
769    Io(#[from] std::io::Error),
770    #[error("Serialization error: {0}")]
771    Serialization(#[from] serde_json::Error),
772    #[error("Invalid crashpack format: {0}")]
773    InvalidFormat(String),
774}