Skip to main content

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