ferrous_forge/safety/
mod.rs

1//! Enhanced Safety Pipeline for Ferrous Forge
2//!
3//! This module implements a comprehensive safety pipeline that prevents broken code
4//! from reaching GitHub or crates.io by running mandatory checks before git operations
5//! and cargo publish commands.
6
7use crate::{Error, Result};
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11pub mod bypass;
12pub mod checks;
13pub mod config;
14// pub mod installer;  // TODO: Implement installer
15pub mod pipeline;
16pub mod report;
17
18pub use config::SafetyConfig;
19pub use pipeline::SafetyPipeline;
20pub use report::{CheckResult, SafetyReport};
21
22/// Pipeline stage for safety checks
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum PipelineStage {
25    /// Pre-commit checks (fast, essential)
26    PreCommit,
27    /// Pre-push checks (comprehensive)
28    PrePush,
29    /// Publish checks (exhaustive)
30    Publish,
31}
32
33impl PipelineStage {
34    /// Get the stage name as a string
35    pub fn name(&self) -> &'static str {
36        match self {
37            Self::PreCommit => "pre-commit",
38            Self::PrePush => "pre-push",
39            Self::Publish => "publish",
40        }
41    }
42
43    /// Get the display name for the stage
44    pub fn display_name(&self) -> &'static str {
45        match self {
46            Self::PreCommit => "Pre-Commit",
47            Self::PrePush => "Pre-Push",
48            Self::Publish => "Publish",
49        }
50    }
51
52    /// Get the timeout for this stage
53    pub fn default_timeout(&self) -> Duration {
54        match self {
55            Self::PreCommit => Duration::from_secs(300), // 5 minutes
56            Self::PrePush => Duration::from_secs(600),   // 10 minutes
57            Self::Publish => Duration::from_secs(900),   // 15 minutes
58        }
59    }
60}
61
62impl std::str::FromStr for PipelineStage {
63    type Err = Error;
64
65    fn from_str(s: &str) -> Result<Self> {
66        match s.to_lowercase().as_str() {
67            "pre-commit" | "precommit" | "commit" => Ok(Self::PreCommit),
68            "pre-push" | "prepush" | "push" => Ok(Self::PrePush),
69            "publish" | "pub" => Ok(Self::Publish),
70            _ => Err(Error::parse(format!("Unknown pipeline stage: {}", s))),
71        }
72    }
73}
74
75impl std::fmt::Display for PipelineStage {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.name())
78    }
79}
80
81/// Safety check type
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum CheckType {
84    /// Format checking (cargo fmt --check)
85    Format,
86    /// Clippy linting (cargo clippy -- -D warnings)
87    Clippy,
88    /// Build checking (cargo build --release)
89    Build,
90    /// Test execution (cargo test --all-features)
91    Test,
92    /// Security audit (cargo audit)
93    Audit,
94    /// Documentation build (cargo doc)
95    Doc,
96    /// Publish dry run (cargo publish --dry-run)
97    PublishDryRun,
98    /// Ferrous Forge standards validation
99    Standards,
100    /// Documentation coverage check
101    DocCoverage,
102    /// License validation
103    License,
104    /// Semver compatibility check
105    Semver,
106}
107
108impl CheckType {
109    /// Get the check name as a string
110    pub fn name(&self) -> &'static str {
111        match self {
112            Self::Format => "format",
113            Self::Clippy => "clippy",
114            Self::Build => "build",
115            Self::Test => "test",
116            Self::Audit => "audit",
117            Self::Doc => "doc",
118            Self::PublishDryRun => "publish-dry-run",
119            Self::Standards => "standards",
120            Self::DocCoverage => "doc-coverage",
121            Self::License => "license",
122            Self::Semver => "semver",
123        }
124    }
125
126    /// Get the display name for the check
127    pub fn display_name(&self) -> &'static str {
128        match self {
129            Self::Format => "Format Check",
130            Self::Clippy => "Clippy Check",
131            Self::Build => "Build Check",
132            Self::Test => "Test Check",
133            Self::Audit => "Security Audit",
134            Self::Doc => "Documentation Build",
135            Self::PublishDryRun => "Publish Dry Run",
136            Self::Standards => "Standards Check",
137            Self::DocCoverage => "Documentation Coverage",
138            Self::License => "License Check",
139            Self::Semver => "Semver Check",
140        }
141    }
142
143    /// Get the checks for a specific pipeline stage
144    pub fn for_stage(stage: PipelineStage) -> Vec<Self> {
145        match stage {
146            PipelineStage::PreCommit => {
147                vec![Self::Format, Self::Clippy, Self::Build, Self::Standards]
148            }
149            PipelineStage::PrePush => vec![
150                Self::Format,
151                Self::Clippy,
152                Self::Build,
153                Self::Standards,
154                Self::Test,
155                Self::Audit,
156                Self::Doc,
157            ],
158            PipelineStage::Publish => vec![
159                Self::Format,
160                Self::Clippy,
161                Self::Build,
162                Self::Standards,
163                Self::Test,
164                Self::Audit,
165                Self::Doc,
166                Self::PublishDryRun,
167                Self::DocCoverage,
168                Self::License,
169                Self::Semver,
170            ],
171        }
172    }
173}
174
175/// Safety enforcement result
176#[derive(Debug, Clone)]
177pub enum SafetyResult {
178    /// All checks passed - operation allowed
179    Passed,
180    /// Checks failed - operation blocked
181    Blocked {
182        /// Failed checks
183        failures: Vec<String>,
184        /// Suggestions for fixes
185        suggestions: Vec<String>,
186    },
187    /// Checks bypassed - operation allowed with warning
188    Bypassed {
189        /// Reason for bypass
190        reason: String,
191        /// Who bypassed
192        user: String,
193    },
194}
195
196impl SafetyResult {
197    /// Check if the operation should be allowed
198    pub fn is_allowed(&self) -> bool {
199        matches!(self, Self::Passed | Self::Bypassed { .. })
200    }
201
202    /// Get a user-friendly message
203    pub fn message(&self) -> String {
204        match self {
205            Self::Passed => "🎉 All safety checks passed! Operation allowed.".to_string(),
206            Self::Blocked {
207                failures,
208                suggestions,
209            } => {
210                let mut msg = "🚨 Safety checks FAILED - operation blocked!\n\n".to_string();
211
212                if !failures.is_empty() {
213                    msg.push_str("Failures:\n");
214                    for failure in failures {
215                        msg.push_str(&format!("  • {}\n", failure));
216                    }
217                }
218
219                if !suggestions.is_empty() {
220                    msg.push_str("\nSuggestions:\n");
221                    for suggestion in suggestions {
222                        msg.push_str(&format!("  • {}\n", suggestion));
223                    }
224                }
225
226                msg
227            }
228            Self::Bypassed { reason, user } => {
229                format!(
230                    "⚠️  Safety checks bypassed by {} - reason: {}",
231                    user, reason
232                )
233            }
234        }
235    }
236}
237
238#[cfg(test)]
239#[allow(clippy::unwrap_used, clippy::expect_used)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_pipeline_stage_from_str() {
245        assert_eq!(
246            "pre-commit".parse::<PipelineStage>().unwrap(),
247            PipelineStage::PreCommit
248        );
249        assert_eq!(
250            "pre-push".parse::<PipelineStage>().unwrap(),
251            PipelineStage::PrePush
252        );
253        assert_eq!(
254            "publish".parse::<PipelineStage>().unwrap(),
255            PipelineStage::Publish
256        );
257        assert!("invalid".parse::<PipelineStage>().is_err());
258    }
259
260    #[test]
261    fn test_check_type_for_stage() {
262        let pre_commit_checks = CheckType::for_stage(PipelineStage::PreCommit);
263        assert!(pre_commit_checks.contains(&CheckType::Format));
264        assert!(pre_commit_checks.contains(&CheckType::Clippy));
265        assert!(!pre_commit_checks.contains(&CheckType::Test)); // Not in pre-commit
266
267        let publish_checks = CheckType::for_stage(PipelineStage::Publish);
268        assert!(publish_checks.contains(&CheckType::PublishDryRun));
269        assert!(publish_checks.contains(&CheckType::Semver));
270    }
271
272    #[test]
273    fn test_safety_result_is_allowed() {
274        assert!(SafetyResult::Passed.is_allowed());
275        assert!(SafetyResult::Bypassed {
276            reason: "test".to_string(),
277            user: "test".to_string()
278        }
279        .is_allowed());
280        assert!(!SafetyResult::Blocked {
281            failures: vec![],
282            suggestions: vec![]
283        }
284        .is_allowed());
285    }
286}