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