Skip to main content

torvyn_cli/commands/
check.rs

1//! `torvyn check` — validate contracts, manifest, and project structure.
2//!
3//! Delegates to `torvyn-contracts` for WIT validation and `torvyn-config`
4//! for manifest parsing and semantic validation.
5
6use crate::cli::CheckArgs;
7use crate::errors::CliError;
8use crate::output::terminal;
9use crate::output::{CommandResult, HumanRenderable, OutputContext};
10use serde::Serialize;
11use std::path::Path;
12
13/// Result of a `torvyn check`.
14#[derive(Debug, Serialize)]
15pub struct CheckResult {
16    /// Whether all checks passed.
17    pub all_passed: bool,
18    /// Number of WIT files parsed.
19    pub wit_files_parsed: usize,
20    /// Number of errors found.
21    pub error_count: usize,
22    /// Number of warnings found.
23    pub warning_count: usize,
24    /// Detailed diagnostics.
25    pub diagnostics: Vec<CheckDiagnostic>,
26}
27
28/// A single check diagnostic.
29#[derive(Debug, Serialize, Clone)]
30pub struct CheckDiagnostic {
31    /// Severity: "error" or "warning".
32    pub severity: String,
33    /// Error code (e.g., "E0201").
34    pub code: String,
35    /// What went wrong.
36    pub message: String,
37    /// File and location (if applicable).
38    pub location: Option<String>,
39    /// Suggested fix.
40    pub help: Option<String>,
41}
42
43impl HumanRenderable for CheckResult {
44    fn render_human(&self, ctx: &OutputContext) {
45        if self.all_passed {
46            terminal::print_success(ctx, "Torvyn.toml is valid");
47            terminal::print_success(
48                ctx,
49                &format!(
50                    "WIT contracts parsed ({} file(s), 0 errors)",
51                    self.wit_files_parsed
52                ),
53            );
54            terminal::print_success(ctx, "World definition resolves correctly");
55            terminal::print_success(ctx, "Capability declarations consistent");
56            eprintln!();
57            eprintln!("  All checks passed.");
58        } else {
59            for d in &self.diagnostics {
60                let prefix = if d.severity == "error" {
61                    if ctx.color_enabled {
62                        format!(
63                            "{}",
64                            console::style(format!("error[{}]:", d.code)).red().bold()
65                        )
66                    } else {
67                        format!("error[{}]:", d.code)
68                    }
69                } else if ctx.color_enabled {
70                    format!(
71                        "{}",
72                        console::style(format!("warning[{}]:", d.code))
73                            .yellow()
74                            .bold()
75                    )
76                } else {
77                    format!("warning[{}]:", d.code)
78                };
79                eprintln!("\n{prefix} {}", d.message);
80                if let Some(loc) = &d.location {
81                    eprintln!("  --> {loc}");
82                }
83                if let Some(help) = &d.help {
84                    if ctx.color_enabled {
85                        eprintln!("  {} {help}", console::style("help:").cyan().bold());
86                    } else {
87                        eprintln!("  help: {help}");
88                    }
89                }
90            }
91            eprintln!(
92                "\n  {} error(s), {} warning(s)",
93                self.error_count, self.warning_count
94            );
95        }
96    }
97}
98
99/// Execute the `torvyn check` command.
100///
101/// COLD PATH.
102pub async fn execute(
103    args: &CheckArgs,
104    ctx: &OutputContext,
105) -> Result<CommandResult<CheckResult>, CliError> {
106    let manifest_path = &args.manifest;
107
108    // Verify manifest exists
109    if !manifest_path.exists() {
110        return Err(CliError::Config {
111            detail: format!("Manifest not found: {}", manifest_path.display()),
112            file: Some(manifest_path.display().to_string()),
113            suggestion:
114                "Run this command from a Torvyn project directory, or use --manifest <path>.".into(),
115        });
116    }
117
118    ctx.print_debug(&format!("Checking manifest: {}", manifest_path.display()));
119
120    let mut diagnostics = Vec::new();
121    let mut wit_files_parsed = 0;
122
123    // Step 1: Parse and validate manifest via torvyn-config
124    let manifest_content = std::fs::read_to_string(manifest_path).map_err(|e| CliError::Io {
125        detail: format!("Cannot read manifest: {e}"),
126        path: Some(manifest_path.display().to_string()),
127    })?;
128
129    match torvyn_config::ComponentManifest::from_toml_str(
130        &manifest_content,
131        manifest_path.to_str().unwrap_or("Torvyn.toml"),
132    ) {
133        Ok(_manifest) => {
134            ctx.print_debug("Manifest parsed successfully");
135        }
136        Err(errors) => {
137            for err in &errors {
138                diagnostics.push(CheckDiagnostic {
139                    severity: "error".into(),
140                    code: err.code.to_string(),
141                    message: err.message.clone(),
142                    location: Some(err.file.clone()),
143                    help: if err.suggestion.is_empty() {
144                        None
145                    } else {
146                        Some(err.suggestion.clone())
147                    },
148                });
149            }
150        }
151    }
152
153    // Step 2: Find and validate WIT files via torvyn-contracts
154    let project_dir = manifest_path.parent().unwrap_or(Path::new("."));
155    let wit_dir = project_dir.join("wit");
156
157    if wit_dir.exists() {
158        // Count WIT files
159        if let Ok(entries) = std::fs::read_dir(&wit_dir) {
160            for entry in entries.flatten() {
161                if entry
162                    .path()
163                    .extension()
164                    .map(|e| e == "wit")
165                    .unwrap_or(false)
166                {
167                    wit_files_parsed += 1;
168                }
169            }
170        }
171
172        // Validate using torvyn-contracts with real WIT parser
173        let wit_parser = torvyn_contracts::WitParserImpl::new();
174        let result = torvyn_contracts::validate_component(project_dir, &wit_parser);
175        for diag in &result.diagnostics {
176            let severity = match diag.severity {
177                torvyn_contracts::Severity::Error => "error",
178                torvyn_contracts::Severity::Warning => "warning",
179                torvyn_contracts::Severity::Hint => "warning",
180            };
181
182            let location = diag.locations.first().map(|l| {
183                format!(
184                    "{}:{}:{}",
185                    l.location.file.display(),
186                    l.location.line,
187                    l.location.column
188                )
189            });
190
191            diagnostics.push(CheckDiagnostic {
192                severity: severity.into(),
193                code: format!("{}", diag.code),
194                message: diag.message.clone(),
195                location,
196                help: diag.help.clone(),
197            });
198        }
199    } else {
200        diagnostics.push(CheckDiagnostic {
201            severity: "warning".into(),
202            code: "E0100".into(),
203            message: "No wit/ directory found".into(),
204            location: Some(project_dir.display().to_string()),
205            help: Some("Create a wit/ directory with your component's world definition.".into()),
206        });
207    }
208
209    let error_count = diagnostics.iter().filter(|d| d.severity == "error").count();
210    let warning_count = diagnostics
211        .iter()
212        .filter(|d| d.severity == "warning")
213        .count();
214
215    let all_passed = if args.strict {
216        error_count == 0 && warning_count == 0
217    } else {
218        error_count == 0
219    };
220
221    let result = CheckResult {
222        all_passed,
223        wit_files_parsed,
224        error_count,
225        warning_count,
226        diagnostics: diagnostics.clone(),
227    };
228
229    if !all_passed {
230        return Err(CliError::Contract {
231            detail: format!("{error_count} error(s) found during validation"),
232            diagnostics: result
233                .diagnostics
234                .iter()
235                .filter(|d| d.severity == "error")
236                .map(|d| d.message.clone())
237                .collect(),
238        });
239    }
240
241    Ok(CommandResult {
242        success: true,
243        command: "check".into(),
244        data: result,
245        warnings: vec![],
246    })
247}