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