Skip to main content

ferrous_forge/cargo_intercept/
mod.rs

1//! Cargo publish interception system
2//!
3//! This module provides functionality to intercept `cargo publish` commands
4//! and run Ferrous Forge validation before allowing publication to crates.io.
5//!
6//! ## Tiered Blocking Behavior
7//!
8//! - **Edition/version violations** → block ALL cargo commands (build, test, run, check, publish)
9//! - **Code style violations** (file size, function size) → WARN during dev commands, block at publish
10//! - `FERROUS_FORGE_BYPASS=true` → skips style checks; edition/version still
11//!   enforced
12//! - `FERROUS_FORGE_FORCE_BYPASS=true` → absolute override with visible "BYPASSED" warning
13
14/// Validation logic for intercepted cargo commands.
15pub mod validation;
16/// Cargo command wrapper for transparent interception.
17pub mod wrapper;
18
19use crate::Result;
20use crate::validation::ViolationType;
21use std::env;
22use std::path::Path;
23
24/// Intercepts and validates cargo publish commands
25pub struct CargoInterceptor {
26    /// Whether to enforce dogfooding (using Ferrous Forge on itself)
27    enforce_dogfooding: bool,
28    /// Skip style checks (edition/version still enforced)
29    bypass_style: bool,
30    /// Absolute override — skip all checks (with warning)
31    force_bypass: bool,
32}
33
34impl Default for CargoInterceptor {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl CargoInterceptor {
41    /// Create a new cargo interceptor, reading bypass state from environment
42    pub fn new() -> Self {
43        let bypass_style = env::var("FERROUS_FORGE_BYPASS")
44            .unwrap_or_default()
45            .eq_ignore_ascii_case("true");
46
47        let force_bypass = env::var("FERROUS_FORGE_FORCE_BYPASS")
48            .unwrap_or_default()
49            .eq_ignore_ascii_case("true");
50
51        Self {
52            enforce_dogfooding: true,
53            bypass_style,
54            force_bypass,
55        }
56    }
57
58    /// Create interceptor with custom settings
59    pub fn with_dogfooding(enforce_dogfooding: bool) -> Self {
60        let mut interceptor = Self::new();
61        interceptor.enforce_dogfooding = enforce_dogfooding;
62        interceptor
63    }
64}
65
66/// Intercept cargo publish command and run full validation (all violations block)
67///
68/// # Errors
69///
70/// Returns [`crate::Error::Validation`] if pre-publish validation or version consistency
71/// checks fail. Returns [`crate::Error::Standards`] if dogfooding enforcement detects
72/// violations.
73pub async fn intercept_publish_command(project_path: &Path) -> Result<()> {
74    let interceptor = CargoInterceptor::new();
75
76    if interceptor.force_bypass {
77        eprintln!(
78            "\n⚠️  FERROUS FORGE FORCE BYPASSED — FERROUS_FORGE_FORCE_BYPASS=true\n\
79             All validation skipped. This should NEVER happen in production.\n"
80        );
81        return Ok(());
82    }
83
84    if interceptor.bypass_style {
85        tracing::warn!(
86            "FERROUS_FORGE_BYPASS enabled — style checks skipped, locked settings still enforced"
87        );
88    }
89
90    tracing::info!("Intercepting cargo publish — running validation");
91
92    // For publish: ALL violations block (both locked and style)
93    validation::pre_publish_validation(project_path).await?;
94    validation::version_consistency_check(project_path)?;
95
96    if interceptor.enforce_dogfooding {
97        validation::enforce_dogfooding(project_path).await?;
98    }
99
100    tracing::info!("Pre-publish validation passed");
101    Ok(())
102}
103
104/// Intercept dev commands (build, test, run, check) with tiered blocking:
105/// - Locked settings (edition/version) → ALWAYS block
106/// - Style violations → WARN only (unless `enforce_style` is true)
107///
108/// # Errors
109///
110/// Returns [`crate::Error::Validation`] if locked settings (edition, rust-version) are
111/// violated. Style violations produce warnings but do not return errors.
112pub async fn intercept_dev_command(project_path: &Path) -> Result<()> {
113    let interceptor = CargoInterceptor::new();
114
115    if interceptor.force_bypass {
116        eprintln!(
117            "\n⚠️  FERROUS FORGE FORCE BYPASSED — FERROUS_FORGE_FORCE_BYPASS=true\n\
118             All validation skipped. This should NEVER happen in production.\n"
119        );
120        return Ok(());
121    }
122
123    // Check locked settings violations first — these always block
124    let locked_violations = validation::check_locked_settings(project_path).await?;
125    if !locked_violations.is_empty() {
126        eprintln!("\n❌ FERROUS FORGE — Locked Setting Violations Detected\n");
127        for v in &locked_violations {
128            eprintln!("{}\n", v.message);
129        }
130        return Err(crate::Error::validation(
131            "Locked setting violations must be resolved before building. \
132             See messages above. DO NOT change locked values — escalate to human.",
133        ));
134    }
135
136    // Style violations — warn but don't block during dev (unless bypass disabled)
137    if !interceptor.bypass_style {
138        let style_violations = validation::check_style_violations(project_path).await?;
139        if !style_violations.is_empty() {
140            eprintln!(
141                "\n⚠️  Ferrous Forge style warnings ({} violations):",
142                style_violations.len()
143            );
144            for v in style_violations.iter().take(5) {
145                eprintln!(
146                    "   {:?}: {}:{} — {}",
147                    v.violation_type,
148                    v.file.display(),
149                    v.line,
150                    v.message.lines().next().unwrap_or("")
151                );
152            }
153            if style_violations.len() > 5 {
154                eprintln!(
155                    "   ... and {} more (run 'ferrous-forge validate' \
156                 for full list)",
157                    style_violations.len() - 5
158                );
159            }
160            eprintln!("   (These will block 'cargo publish'. Fix before publishing.)");
161            eprintln!("   (Set FERROUS_FORGE_BYPASS=true to suppress these warnings.)\n");
162        }
163    } else {
164        tracing::info!(
165            "FERROUS_FORGE_BYPASS — style warnings suppressed (locked settings still checked)"
166        );
167    }
168
169    Ok(())
170}
171
172/// Check if violations include any locked setting violations
173pub fn has_locked_violations(violations: &[crate::validation::Violation]) -> bool {
174    violations.iter().any(|v| {
175        matches!(
176            v.violation_type,
177            ViolationType::WrongEdition
178                | ViolationType::OldRustVersion
179                | ViolationType::LockedSetting
180        )
181    })
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_cargo_interceptor_creation() {
190        let interceptor = CargoInterceptor::new();
191        assert!(interceptor.enforce_dogfooding);
192    }
193}