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