Skip to main content

debtmap/effects/
validation.rs

1//! Expanded validation patterns using stillwater's predicate combinators.
2//!
3//! This module provides validation patterns for:
4//! - Analysis results validation with error accumulation
5//! - Predicate-based debt detection rules
6//! - File processing validation with partial success semantics
7//! - Field context for structured error reporting (Spec 003)
8//!
9//! # Predicate Combinators
10//!
11//! Use stillwater's predicate module for composable validation rules:
12//!
13//! ```rust,ignore
14//! use stillwater::predicate::*;
15//! use debtmap::effects::validation::*;
16//!
17//! // Define complexity thresholds as composable predicates
18//! let warning_zone = gt(20_u32).and(lt(100_u32));
19//! let critical_zone = ge(100_u32);
20//!
21//! // Apply to values using ensure extension
22//! let validated = complexity_value.ensure(lt(100_u32), "Complexity too high");
23//! ```
24//!
25//! # EnsureExt Trait
26//!
27//! The `EnsureExt` trait provides a fluent API for applying predicates:
28//!
29//! ```rust,ignore
30//! use debtmap::effects::validation::EnsureExt;
31//! use debtmap::errors::AnalysisError;
32//!
33//! let validated = file_length
34//!     .ensure(le(500_usize), AnalysisError::validation("File too long"));
35//! ```
36//!
37//! # Field Context (Spec 003)
38//!
39//! The module provides types for attaching field context to validation errors:
40//!
41//! ```rust,ignore
42//! use debtmap::effects::validation::{FieldPath, ValidationError};
43//!
44//! // Create a nested field path
45//! let path = FieldPath::root()
46//!     .push("config")
47//!     .push("thresholds")
48//!     .push("cyclomatic");
49//!
50//! // Create error with field context
51//! let error = ValidationError::at_field(&path, "must be greater than zero")
52//!     .with_context("positive integer", "-5");
53//! ```
54//!
55//! # ValidatedFileResults
56//!
57//! Represents the result of validating multiple files with partial success:
58//!
59//! ```rust,ignore
60//! match validate_files(paths)? {
61//!     ValidatedFileResults::AllSucceeded(metrics) => {
62//!         println!("All {} files parsed successfully", metrics.len());
63//!     }
64//!     ValidatedFileResults::PartialSuccess { succeeded, failures } => {
65//!         println!("{} succeeded, {} failed", succeeded.len(), failures.len());
66//!     }
67//! }
68//! ```
69//!
70//! # ValidatedFileSet (Spec 003)
71//!
72//! An alternative to ValidatedFileResults that separates valid files from errors
73//! while supporting partial success with file-specific error context:
74//!
75//! ```rust,ignore
76//! use debtmap::effects::validation::ValidatedFileSet;
77//!
78//! let file_set = parse_all_files(&file_paths);
79//! if file_set.is_partial_success() {
80//!     println!("Processed {} files with {} errors",
81//!         file_set.valid.len(), file_set.errors.len());
82//! }
83//! ```
84
85use crate::core::FileMetrics;
86use crate::effects::{AnalysisValidation, validation_success};
87use crate::errors::AnalysisError;
88use serde::Serialize;
89use std::path::PathBuf;
90use stillwater::predicate::Predicate;
91use stillwater::refined::{FieldError, ValidationFieldExt};
92use stillwater::{NonEmptyVec, Validation};
93
94// =============================================================================
95// Field Context Types (Spec 003)
96// =============================================================================
97
98/// Nested field path for error context.
99///
100/// Tracks the path from root to a specific field in a configuration or data
101/// structure, enabling precise error messages like "config.thresholds.cyclomatic".
102///
103/// # Example
104///
105/// ```rust
106/// use debtmap::effects::validation::FieldPath;
107///
108/// let path = FieldPath::root()
109///     .push("config")
110///     .push("thresholds")
111///     .push("cyclomatic");
112///
113/// assert_eq!(path.as_string(), "config.thresholds.cyclomatic");
114/// ```
115#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
116pub struct FieldPath(Vec<String>);
117
118impl FieldPath {
119    /// Create an empty root path.
120    pub fn root() -> Self {
121        Self(Vec::new())
122    }
123
124    /// Create a path with a single field.
125    pub fn new(field: impl Into<String>) -> Self {
126        Self(vec![field.into()])
127    }
128
129    /// Add a field to the path, returning a new path.
130    pub fn push(&self, field: impl Into<String>) -> Self {
131        let mut path = self.0.clone();
132        path.push(field.into());
133        Self(path)
134    }
135
136    /// Get the path as a dot-separated string.
137    pub fn as_string(&self) -> String {
138        self.0.join(".")
139    }
140
141    /// Check if this is the root path (no fields).
142    pub fn is_root(&self) -> bool {
143        self.0.is_empty()
144    }
145
146    /// Get the number of segments in the path.
147    pub fn len(&self) -> usize {
148        self.0.len()
149    }
150
151    /// Check if the path is empty.
152    pub fn is_empty(&self) -> bool {
153        self.0.is_empty()
154    }
155
156    /// Get the last segment of the path, if any.
157    pub fn last(&self) -> Option<&str> {
158        self.0.last().map(|s| s.as_str())
159    }
160
161    /// Get the segments of the path.
162    pub fn segments(&self) -> &[String] {
163        &self.0
164    }
165}
166
167impl std::fmt::Display for FieldPath {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        write!(f, "{}", self.as_string())
170    }
171}
172
173impl From<&str> for FieldPath {
174    fn from(s: &str) -> Self {
175        Self::new(s)
176    }
177}
178
179impl From<String> for FieldPath {
180    fn from(s: String) -> Self {
181        Self::new(s)
182    }
183}
184
185/// Validation error with full field context.
186///
187/// Provides structured error information including:
188/// - The field path where the error occurred
189/// - A human-readable error message
190/// - Optional expected and actual values for debugging
191///
192/// This type is JSON-serializable for IDE integration and tooling.
193///
194/// # Example
195///
196/// ```rust
197/// use debtmap::effects::validation::{FieldPath, ValidationError};
198///
199/// let error = ValidationError::at_field(
200///     &FieldPath::new("threshold"),
201///     "must be greater than zero"
202/// ).with_context("positive integer", "-5");
203///
204/// assert_eq!(error.field.as_string(), "threshold");
205/// assert_eq!(error.expected, Some("positive integer".to_string()));
206/// assert_eq!(error.actual, Some("-5".to_string()));
207/// ```
208#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
209pub struct ValidationError {
210    /// The field path where the error occurred.
211    pub field: FieldPath,
212    /// Human-readable error message.
213    pub message: String,
214    /// Expected value or constraint (for debugging).
215    pub expected: Option<String>,
216    /// Actual value that failed validation (for debugging).
217    pub actual: Option<String>,
218}
219
220impl ValidationError {
221    /// Create a validation error at a specific field.
222    pub fn at_field(field: &FieldPath, message: impl Into<String>) -> Self {
223        Self {
224            field: field.clone(),
225            message: message.into(),
226            expected: None,
227            actual: None,
228        }
229    }
230
231    /// Create a validation error with a simple field name.
232    pub fn for_field(field: impl Into<String>, message: impl Into<String>) -> Self {
233        Self {
234            field: FieldPath::new(field),
235            message: message.into(),
236            expected: None,
237            actual: None,
238        }
239    }
240
241    /// Add expected and actual context to the error.
242    pub fn with_context(mut self, expected: impl Into<String>, actual: impl Into<String>) -> Self {
243        self.expected = Some(expected.into());
244        self.actual = Some(actual.into());
245        self
246    }
247
248    /// Add expected value context.
249    pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
250        self.expected = Some(expected.into());
251        self
252    }
253
254    /// Add actual value context.
255    pub fn with_actual(mut self, actual: impl Into<String>) -> Self {
256        self.actual = Some(actual.into());
257        self
258    }
259}
260
261impl std::fmt::Display for ValidationError {
262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263        if self.field.is_root() {
264            write!(f, "{}", self.message)?;
265        } else {
266            write!(f, "{}: {}", self.field, self.message)?;
267        }
268
269        if let (Some(expected), Some(actual)) = (&self.expected, &self.actual) {
270            write!(f, " (expected: {}, got: {})", expected, actual)?;
271        } else if let Some(expected) = &self.expected {
272            write!(f, " (expected: {})", expected)?;
273        } else if let Some(actual) = &self.actual {
274            write!(f, " (got: {})", actual)?;
275        }
276
277        Ok(())
278    }
279}
280
281impl std::error::Error for ValidationError {}
282
283/// Error for a specific file with location context.
284///
285/// Tracks file path and optional line/column information for errors
286/// that occur during file processing (parsing, analysis, etc.).
287///
288/// # Example
289///
290/// ```rust
291/// use debtmap::effects::validation::FileError;
292/// use std::path::PathBuf;
293///
294/// let error = FileError::new(
295///     PathBuf::from("src/main.rs"),
296///     "unexpected token 'foo'"
297/// ).at_location(42, 15);
298///
299/// assert_eq!(error.path, PathBuf::from("src/main.rs"));
300/// assert_eq!(error.line, Some(42));
301/// assert_eq!(error.column, Some(15));
302/// ```
303#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
304pub struct FileError {
305    /// Path to the file where the error occurred.
306    pub path: PathBuf,
307    /// Line number where the error occurred (1-indexed).
308    pub line: Option<u32>,
309    /// Column number where the error occurred (1-indexed).
310    pub column: Option<u32>,
311    /// Human-readable error message.
312    pub message: String,
313    /// Optional error code for programmatic handling.
314    pub error_code: Option<String>,
315}
316
317impl FileError {
318    /// Create a new file error with path and message.
319    pub fn new(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
320        Self {
321            path: path.into(),
322            line: None,
323            column: None,
324            message: message.into(),
325            error_code: None,
326        }
327    }
328
329    /// Add line and column location information.
330    pub fn at_location(mut self, line: u32, column: u32) -> Self {
331        self.line = Some(line);
332        self.column = Some(column);
333        self
334    }
335
336    /// Add just line location information.
337    pub fn at_line(mut self, line: u32) -> Self {
338        self.line = Some(line);
339        self
340    }
341
342    /// Add an error code for programmatic handling.
343    pub fn with_code(mut self, code: impl Into<String>) -> Self {
344        self.error_code = Some(code.into());
345        self
346    }
347
348    /// Convert from a parse error with context.
349    pub fn from_parse_error(path: impl Into<PathBuf>, error: impl std::fmt::Display) -> Self {
350        Self::new(path, error.to_string()).with_code("E010")
351    }
352
353    /// Convert from an AnalysisError with path context.
354    pub fn from_analysis_error(path: impl Into<PathBuf>, error: &AnalysisError) -> Self {
355        let path = path.into();
356        let message = error.to_string();
357
358        // Extract line number from the error if it's a parse error
359        let line = if let AnalysisError::ParseError { line, .. } = error {
360            *line
361        } else {
362            None
363        };
364
365        let mut file_error = Self::new(path, message);
366        if let Some(l) = line {
367            file_error.line = Some(l as u32);
368        }
369
370        file_error
371    }
372}
373
374impl std::fmt::Display for FileError {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        write!(f, "{}", self.path.display())?;
377        if let Some(line) = self.line {
378            write!(f, ":{}", line)?;
379            if let Some(column) = self.column {
380                write!(f, ":{}", column)?;
381            }
382        }
383        write!(f, ": {}", self.message)?;
384        if let Some(code) = &self.error_code {
385            write!(f, " [{}]", code)?;
386        }
387        Ok(())
388    }
389}
390
391impl std::error::Error for FileError {}
392
393impl From<FileError> for AnalysisError {
394    fn from(err: FileError) -> Self {
395        AnalysisError::parse_with_path(&err.message, &err.path)
396    }
397}
398
399/// Result of validating multiple files with partial success semantics.
400///
401/// Unlike `ValidatedFileResults`, this type uses generic file data and
402/// provides richer error information with `FileError` instead of `AnalysisError`.
403///
404/// # Example
405///
406/// ```rust
407/// use debtmap::effects::validation::{ValidatedFileSet, FileError};
408/// use std::path::PathBuf;
409///
410/// // Create a partial success result
411/// let file_set = ValidatedFileSet {
412///     valid: vec!["file1 content".to_string(), "file2 content".to_string()],
413///     errors: vec![FileError::new(PathBuf::from("bad.rs"), "parse error")],
414/// };
415///
416/// assert!(file_set.is_partial_success());
417/// assert_eq!(file_set.valid.len(), 2);
418/// assert_eq!(file_set.errors.len(), 1);
419/// ```
420#[derive(Clone, Debug, Default)]
421pub struct ValidatedFileSet<T> {
422    /// Successfully processed files.
423    pub valid: Vec<T>,
424    /// Files that failed to process with their errors.
425    pub errors: Vec<FileError>,
426}
427
428impl<T> ValidatedFileSet<T> {
429    /// Create an empty file set.
430    pub fn empty() -> Self {
431        Self {
432            valid: Vec::new(),
433            errors: Vec::new(),
434        }
435    }
436
437    /// Create a file set with only valid files.
438    pub fn all_valid(valid: Vec<T>) -> Self {
439        Self {
440            valid,
441            errors: Vec::new(),
442        }
443    }
444
445    /// Create a file set with only errors.
446    pub fn all_errors(errors: Vec<FileError>) -> Self {
447        Self {
448            valid: Vec::new(),
449            errors,
450        }
451    }
452
453    /// Check if there are any errors.
454    pub fn has_errors(&self) -> bool {
455        !self.errors.is_empty()
456    }
457
458    /// Check if there are any valid files.
459    pub fn has_valid(&self) -> bool {
460        !self.valid.is_empty()
461    }
462
463    /// Check if this is a partial success (some valid, some errors).
464    pub fn is_partial_success(&self) -> bool {
465        self.has_valid() && self.has_errors()
466    }
467
468    /// Check if all files succeeded.
469    pub fn is_all_success(&self) -> bool {
470        self.has_valid() && !self.has_errors()
471    }
472
473    /// Check if all files failed.
474    pub fn is_all_failed(&self) -> bool {
475        !self.has_valid() && self.has_errors()
476    }
477
478    /// Get the number of successfully processed files.
479    pub fn valid_count(&self) -> usize {
480        self.valid.len()
481    }
482
483    /// Get the number of errors.
484    pub fn error_count(&self) -> usize {
485        self.errors.len()
486    }
487
488    /// Convert to a strict Result (any error = failure).
489    pub fn into_strict_result(self) -> Result<Vec<T>, Vec<FileError>> {
490        if self.errors.is_empty() {
491            Ok(self.valid)
492        } else {
493            Err(self.errors)
494        }
495    }
496
497    /// Convert to a lenient Result (only fail if all files failed).
498    pub fn into_lenient_result(self) -> Result<Vec<T>, Vec<FileError>> {
499        if self.valid.is_empty() && !self.errors.is_empty() {
500            Err(self.errors)
501        } else {
502            Ok(self.valid)
503        }
504    }
505
506    /// Add a valid file to the set.
507    pub fn add_valid(&mut self, item: T) {
508        self.valid.push(item);
509    }
510
511    /// Add an error to the set.
512    pub fn add_error(&mut self, error: FileError) {
513        self.errors.push(error);
514    }
515
516    /// Merge another file set into this one.
517    pub fn merge(&mut self, other: ValidatedFileSet<T>) {
518        self.valid.extend(other.valid);
519        self.errors.extend(other.errors);
520    }
521}
522
523impl<T: Serialize> Serialize for ValidatedFileSet<T> {
524    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
525    where
526        S: serde::Serializer,
527    {
528        use serde::ser::SerializeStruct;
529
530        let mut state = serializer.serialize_struct("ValidatedFileSet", 4)?;
531        state.serialize_field("valid_count", &self.valid.len())?;
532        state.serialize_field("error_count", &self.errors.len())?;
533        state.serialize_field("valid", &self.valid)?;
534        state.serialize_field("errors", &self.errors)?;
535        state.end()
536    }
537}
538
539// =============================================================================
540// Field Context Extension Traits
541// =============================================================================
542
543/// Extension trait for adding field context to validations.
544///
545/// This trait extends stillwater's `Validation` type with methods for
546/// attaching field paths to errors.
547pub trait FieldContextExt<T, E> {
548    /// Attach a field path to validation errors.
549    ///
550    /// # Example
551    ///
552    /// ```rust,ignore
553    /// let validated = validate_threshold(config.threshold)
554    ///     .with_field_path(&FieldPath::new("config.threshold"));
555    /// ```
556    fn with_field_path(self, path: &FieldPath) -> Validation<T, NonEmptyVec<ValidationError>>
557    where
558        E: std::fmt::Display;
559
560    /// Attach a simple field name to validation errors.
561    ///
562    /// # Example
563    ///
564    /// ```rust,ignore
565    /// let validated = validate_threshold(value)
566    ///     .with_field_name("threshold");
567    /// ```
568    fn with_field_name(self, field: &str) -> Validation<T, NonEmptyVec<ValidationError>>
569    where
570        E: std::fmt::Display;
571}
572
573impl<T, E> FieldContextExt<T, E> for Validation<T, NonEmptyVec<E>> {
574    fn with_field_path(self, path: &FieldPath) -> Validation<T, NonEmptyVec<ValidationError>>
575    where
576        E: std::fmt::Display,
577    {
578        match self {
579            Validation::Success(value) => Validation::Success(value),
580            Validation::Failure(errors) => {
581                let field_errors: Vec<ValidationError> = errors
582                    .into_iter()
583                    .map(|e| ValidationError::at_field(path, e.to_string()))
584                    .collect();
585                Validation::Failure(
586                    NonEmptyVec::from_vec(field_errors).expect("errors came from non-empty vec"),
587                )
588            }
589        }
590    }
591
592    fn with_field_name(self, field: &str) -> Validation<T, NonEmptyVec<ValidationError>>
593    where
594        E: std::fmt::Display,
595    {
596        self.with_field_path(&FieldPath::new(field))
597    }
598}
599
600// Re-export stillwater's field types for convenience
601pub use stillwater::refined::FieldError as StillwaterFieldError;
602
603/// Create a validation that wraps errors with field context using stillwater's FieldError.
604pub fn validate_field<T, E>(
605    field: &'static str,
606    validation: Validation<T, E>,
607) -> Validation<T, FieldError<E>> {
608    validation.with_field(field)
609}
610
611// =============================================================================
612// EnsureExt Trait
613// =============================================================================
614
615/// Extension trait for applying predicates to values with validation.
616///
617/// This trait provides a fluent API for validating values against predicates,
618/// returning `Validation` results that can accumulate errors.
619///
620/// # Example
621///
622/// ```rust
623/// use debtmap::effects::validation::EnsureExt;
624/// use debtmap::errors::AnalysisError;
625/// use stillwater::predicate::lt;
626///
627/// let complexity: u32 = 50;
628/// let validated = complexity.ensure(lt(100_u32), AnalysisError::validation("Complexity too high"));
629/// assert!(validated.is_success());
630///
631/// let high_complexity: u32 = 150;
632/// let validated = high_complexity.ensure(lt(100_u32), AnalysisError::validation("Complexity too high"));
633/// assert!(validated.is_failure());
634/// ```
635pub trait EnsureExt<T> {
636    /// Validate that this value satisfies the given predicate.
637    ///
638    /// Returns `Validation::Success(self)` if the predicate passes,
639    /// or `Validation::Failure` with the provided error if it fails.
640    fn ensure<P, E>(self, predicate: P, error: E) -> Validation<T, NonEmptyVec<E>>
641    where
642        P: Predicate<T>;
643
644    /// Validate with an error-generating function.
645    ///
646    /// The function receives a reference to the value and generates
647    /// an appropriate error message.
648    fn ensure_with<P, E, F>(self, predicate: P, error_fn: F) -> Validation<T, NonEmptyVec<E>>
649    where
650        P: Predicate<T>,
651        F: FnOnce(&T) -> E;
652}
653
654impl<T> EnsureExt<T> for T {
655    fn ensure<P, E>(self, predicate: P, error: E) -> Validation<T, NonEmptyVec<E>>
656    where
657        P: Predicate<T>,
658    {
659        if predicate.check(&self) {
660            Validation::Success(self)
661        } else {
662            Validation::Failure(NonEmptyVec::new(error, Vec::new()))
663        }
664    }
665
666    fn ensure_with<P, E, F>(self, predicate: P, error_fn: F) -> Validation<T, NonEmptyVec<E>>
667    where
668        P: Predicate<T>,
669        F: FnOnce(&T) -> E,
670    {
671        if predicate.check(&self) {
672            Validation::Success(self)
673        } else {
674            let error = error_fn(&self);
675            Validation::Failure(NonEmptyVec::new(error, Vec::new()))
676        }
677    }
678}
679
680// =============================================================================
681// ValidatedFileResults
682// =============================================================================
683
684/// Result of validating multiple files with partial success semantics.
685///
686/// This type allows analysis to continue even when some files fail to parse,
687/// collecting both successful results and accumulated errors.
688#[derive(Debug, Clone)]
689pub enum ValidatedFileResults {
690    /// All files parsed and analyzed successfully.
691    AllSucceeded(Vec<FileMetrics>),
692
693    /// Some files succeeded, some failed.
694    /// Analysis can continue with partial results while reporting errors.
695    PartialSuccess {
696        /// Successfully parsed and analyzed files.
697        succeeded: Vec<FileMetrics>,
698        /// Errors from files that failed to parse or analyze.
699        failures: NonEmptyVec<AnalysisError>,
700    },
701
702    /// All files failed to parse or analyze.
703    AllFailed(NonEmptyVec<AnalysisError>),
704}
705
706impl ValidatedFileResults {
707    /// Create a new ValidatedFileResults from a collection of individual validations.
708    pub fn from_validations(validations: Vec<AnalysisValidation<FileMetrics>>) -> Self {
709        let mut succeeded = Vec::new();
710        let mut failures: Vec<AnalysisError> = Vec::new();
711
712        for validation in validations {
713            match validation {
714                Validation::Success(metrics) => succeeded.push(metrics),
715                Validation::Failure(errors) => {
716                    failures.extend(errors);
717                }
718            }
719        }
720
721        match (succeeded.is_empty(), failures.is_empty()) {
722            (false, true) => ValidatedFileResults::AllSucceeded(succeeded),
723            (false, false) => ValidatedFileResults::PartialSuccess {
724                succeeded,
725                failures: NonEmptyVec::from_vec(failures)
726                    .expect("failures cannot be empty when not all succeeded"),
727            },
728            (true, false) => ValidatedFileResults::AllFailed(
729                NonEmptyVec::from_vec(failures).expect("failures cannot be empty when all failed"),
730            ),
731            (true, true) => ValidatedFileResults::AllSucceeded(Vec::new()),
732        }
733    }
734
735    /// Get the successfully parsed files (if any).
736    pub fn succeeded(&self) -> &[FileMetrics] {
737        match self {
738            ValidatedFileResults::AllSucceeded(metrics) => metrics,
739            ValidatedFileResults::PartialSuccess { succeeded, .. } => succeeded,
740            ValidatedFileResults::AllFailed(_) => &[],
741        }
742    }
743
744    /// Get the failures (if any).
745    pub fn failures(&self) -> Option<&NonEmptyVec<AnalysisError>> {
746        match self {
747            ValidatedFileResults::AllSucceeded(_) => None,
748            ValidatedFileResults::PartialSuccess { failures, .. } => Some(failures),
749            ValidatedFileResults::AllFailed(failures) => Some(failures),
750        }
751    }
752
753    /// Check if all files succeeded.
754    pub fn is_all_success(&self) -> bool {
755        matches!(self, ValidatedFileResults::AllSucceeded(_))
756    }
757
758    /// Check if there are any failures.
759    pub fn has_failures(&self) -> bool {
760        !matches!(self, ValidatedFileResults::AllSucceeded(_))
761    }
762
763    /// Convert to a standard Validation, treating any failure as overall failure.
764    pub fn into_validation(self) -> AnalysisValidation<Vec<FileMetrics>> {
765        match self {
766            ValidatedFileResults::AllSucceeded(metrics) => validation_success(metrics),
767            ValidatedFileResults::PartialSuccess { failures, .. } => Validation::Failure(failures),
768            ValidatedFileResults::AllFailed(failures) => Validation::Failure(failures),
769        }
770    }
771
772    /// Convert to Result, including partial successes (lenient mode).
773    ///
774    /// In lenient mode, if some files succeed, return Ok with those files.
775    /// Only fail if ALL files failed.
776    pub fn into_lenient_result(self) -> Result<Vec<FileMetrics>, NonEmptyVec<AnalysisError>> {
777        match self {
778            ValidatedFileResults::AllSucceeded(metrics) => Ok(metrics),
779            ValidatedFileResults::PartialSuccess { succeeded, .. } => Ok(succeeded),
780            ValidatedFileResults::AllFailed(failures) => Err(failures),
781        }
782    }
783}
784
785// =============================================================================
786// Predicate Builders for Debt Detection
787// =============================================================================
788
789/// Predicate builder functions for common validation patterns.
790///
791/// These functions create composable predicates for debt detection rules.
792pub mod predicates {
793    use stillwater::predicate::*;
794
795    /// Create a predicate that checks if complexity is in the warning zone (high but not critical).
796    ///
797    /// Default: complexity between 21 and 99 (inclusive on lower bound, exclusive on upper).
798    pub fn high_complexity(warning_threshold: u32, critical_threshold: u32) -> impl Predicate<u32> {
799        ge(warning_threshold).and(lt(critical_threshold))
800    }
801
802    /// Create a predicate that checks if complexity is critical.
803    ///
804    /// Default: complexity >= 100.
805    pub fn critical_complexity(threshold: u32) -> impl Predicate<u32> {
806        ge(threshold)
807    }
808
809    /// Create a predicate that checks if a value is within acceptable bounds.
810    pub fn within_bounds(min: u32, max: u32) -> impl Predicate<u32> {
811        ge(min).and(le(max))
812    }
813
814    /// Create a predicate that checks if file length is acceptable.
815    pub fn acceptable_file_length(max_length: usize) -> impl Predicate<usize> {
816        le(max_length)
817    }
818
819    /// Create a predicate that checks if nesting depth is acceptable.
820    pub fn acceptable_nesting(max_depth: u32) -> impl Predicate<u32> {
821        le(max_depth)
822    }
823
824    /// Create a predicate that checks if function length is acceptable.
825    pub fn acceptable_function_length(max_lines: usize) -> impl Predicate<usize> {
826        le(max_lines)
827    }
828
829    /// Create a predicate that checks if a string is not empty.
830    pub fn not_empty_string() -> impl Predicate<String> {
831        not_empty()
832    }
833
834    /// Create a predicate that checks if a string length is within bounds.
835    pub fn valid_name_length(min: usize, max: usize) -> impl Predicate<String> {
836        len_between(min, max)
837    }
838}
839
840// =============================================================================
841// ValidationRuleSet
842// =============================================================================
843
844/// Configurable rule set for validation thresholds.
845///
846/// This struct allows dynamic configuration of validation rules,
847/// enabling different strictness levels for different contexts.
848#[derive(Debug, Clone)]
849pub struct ValidationRuleSet {
850    /// Complexity threshold for warnings (default: 21).
851    pub complexity_warning: u32,
852    /// Complexity threshold for critical issues (default: 100).
853    pub complexity_critical: u32,
854    /// Maximum function length in lines (default: 50).
855    pub max_function_length: usize,
856    /// Maximum nesting depth (default: 4).
857    pub max_nesting_depth: u32,
858    /// Maximum file length in lines (default: 1000).
859    pub max_file_length: usize,
860    /// Minimum name length for identifiers (default: 2).
861    pub min_name_length: usize,
862    /// Maximum name length for identifiers (default: 50).
863    pub max_name_length: usize,
864}
865
866impl Default for ValidationRuleSet {
867    fn default() -> Self {
868        Self {
869            complexity_warning: 21,
870            complexity_critical: 100,
871            max_function_length: 50,
872            max_nesting_depth: 4,
873            max_file_length: 1000,
874            min_name_length: 2,
875            max_name_length: 50,
876        }
877    }
878}
879
880impl ValidationRuleSet {
881    /// Create a strict rule set with lower thresholds.
882    pub fn strict() -> Self {
883        Self {
884            complexity_warning: 10,
885            complexity_critical: 50,
886            max_function_length: 20,
887            max_nesting_depth: 2,
888            max_file_length: 500,
889            min_name_length: 3,
890            max_name_length: 30,
891        }
892    }
893
894    /// Create a lenient rule set with higher thresholds.
895    pub fn lenient() -> Self {
896        Self {
897            complexity_warning: 30,
898            complexity_critical: 150,
899            max_function_length: 100,
900            max_nesting_depth: 6,
901            max_file_length: 2000,
902            min_name_length: 1,
903            max_name_length: 100,
904        }
905    }
906
907    /// Check if complexity is in the warning zone.
908    pub fn is_warning_complexity(&self, complexity: u32) -> bool {
909        complexity >= self.complexity_warning && complexity < self.complexity_critical
910    }
911
912    /// Check if complexity is critical.
913    pub fn is_critical_complexity(&self, complexity: u32) -> bool {
914        complexity >= self.complexity_critical
915    }
916
917    /// Check if function length is acceptable.
918    pub fn is_acceptable_function_length(&self, length: usize) -> bool {
919        length <= self.max_function_length
920    }
921
922    /// Check if nesting depth is acceptable.
923    pub fn is_acceptable_nesting(&self, depth: u32) -> bool {
924        depth <= self.max_nesting_depth
925    }
926
927    /// Check if file length is acceptable.
928    pub fn is_acceptable_file_length(&self, length: usize) -> bool {
929        length <= self.max_file_length
930    }
931
932    /// Create a complexity predicate for this rule set.
933    pub fn complexity_predicate(&self) -> impl Predicate<u32> + '_ {
934        use stillwater::predicate::lt;
935        lt(self.complexity_critical)
936    }
937
938    /// Create a function length predicate for this rule set.
939    pub fn function_length_predicate(&self) -> impl Predicate<usize> + '_ {
940        use stillwater::predicate::le;
941        le(self.max_function_length)
942    }
943
944    /// Create a nesting depth predicate for this rule set.
945    pub fn nesting_predicate(&self) -> impl Predicate<u32> + '_ {
946        use stillwater::predicate::le;
947        le(self.max_nesting_depth)
948    }
949
950    /// Create a file length predicate for this rule set.
951    pub fn file_length_predicate(&self) -> impl Predicate<usize> + '_ {
952        use stillwater::predicate::le;
953        le(self.max_file_length)
954    }
955}
956
957// =============================================================================
958// Validation Helpers
959// =============================================================================
960
961/// Validate a function's complexity using the given rule set.
962pub fn validate_function_complexity(
963    function_name: &str,
964    complexity: u32,
965    rules: &ValidationRuleSet,
966) -> AnalysisValidation<u32> {
967    use stillwater::predicate::lt;
968
969    complexity.ensure(
970        lt(rules.complexity_critical),
971        AnalysisError::validation(format!(
972            "Function '{}' has critical complexity: {} (threshold: {})",
973            function_name, complexity, rules.complexity_critical
974        )),
975    )
976}
977
978/// Validate a function's length using the given rule set.
979pub fn validate_function_length(
980    function_name: &str,
981    length: usize,
982    rules: &ValidationRuleSet,
983) -> AnalysisValidation<usize> {
984    use stillwater::predicate::le;
985
986    length.ensure(
987        le(rules.max_function_length),
988        AnalysisError::validation(format!(
989            "Function '{}' is too long: {} lines (max: {})",
990            function_name, length, rules.max_function_length
991        )),
992    )
993}
994
995/// Validate a function's nesting depth using the given rule set.
996pub fn validate_nesting_depth(
997    function_name: &str,
998    depth: u32,
999    rules: &ValidationRuleSet,
1000) -> AnalysisValidation<u32> {
1001    use stillwater::predicate::le;
1002
1003    depth.ensure(
1004        le(rules.max_nesting_depth),
1005        AnalysisError::validation(format!(
1006            "Function '{}' has excessive nesting: {} levels (max: {})",
1007            function_name, depth, rules.max_nesting_depth
1008        )),
1009    )
1010}
1011
1012/// Validate a file's length using the given rule set.
1013pub fn validate_file_length(
1014    file_path: &std::path::Path,
1015    length: usize,
1016    rules: &ValidationRuleSet,
1017) -> AnalysisValidation<usize> {
1018    use stillwater::predicate::le;
1019
1020    length.ensure(
1021        le(rules.max_file_length),
1022        AnalysisError::validation(format!(
1023            "File '{}' is too long: {} lines (max: {})",
1024            file_path.display(),
1025            length,
1026            rules.max_file_length
1027        )),
1028    )
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033    use super::*;
1034    use crate::effects::validation_failure;
1035    use stillwater::predicate::*;
1036
1037    // =========================================================================
1038    // EnsureExt Tests
1039    // =========================================================================
1040
1041    #[test]
1042    fn test_ensure_success() {
1043        let value: u32 = 50;
1044        let result = value.ensure(lt(100_u32), AnalysisError::validation("Too high"));
1045        assert!(result.is_success());
1046        match result {
1047            Validation::Success(v) => assert_eq!(v, 50),
1048            _ => panic!("Expected success"),
1049        }
1050    }
1051
1052    #[test]
1053    fn test_ensure_failure() {
1054        let value: u32 = 150;
1055        let result = value.ensure(lt(100_u32), AnalysisError::validation("Too high"));
1056        assert!(result.is_failure());
1057    }
1058
1059    #[test]
1060    fn test_ensure_with_error_fn() {
1061        let value: u32 = 150;
1062        let result = value.ensure_with(lt(100_u32), |v| {
1063            AnalysisError::validation(format!("Value {} exceeds limit", v))
1064        });
1065        assert!(result.is_failure());
1066        match result {
1067            Validation::Failure(errors) => {
1068                let msg = errors.head().to_string();
1069                assert!(msg.contains("150"));
1070            }
1071            _ => panic!("Expected failure"),
1072        }
1073    }
1074
1075    // =========================================================================
1076    // Predicate Combinator Tests
1077    // =========================================================================
1078
1079    #[test]
1080    fn test_high_complexity_predicate() {
1081        let pred = predicates::high_complexity(21, 100);
1082        assert!(pred.check(&50)); // In warning zone
1083        assert!(pred.check(&21)); // At lower boundary
1084        assert!(pred.check(&99)); // Just below critical
1085        assert!(!pred.check(&20)); // Below warning
1086        assert!(!pred.check(&100)); // At critical (excluded)
1087    }
1088
1089    #[test]
1090    fn test_critical_complexity_predicate() {
1091        let pred = predicates::critical_complexity(100);
1092        assert!(pred.check(&100));
1093        assert!(pred.check(&150));
1094        assert!(!pred.check(&99));
1095    }
1096
1097    #[test]
1098    fn test_within_bounds_predicate() {
1099        let pred = predicates::within_bounds(10, 50);
1100        assert!(pred.check(&30));
1101        assert!(pred.check(&10)); // Lower bound inclusive
1102        assert!(pred.check(&50)); // Upper bound inclusive
1103        assert!(!pred.check(&9));
1104        assert!(!pred.check(&51));
1105    }
1106
1107    #[test]
1108    fn test_acceptable_file_length_predicate() {
1109        let pred = predicates::acceptable_file_length(1000);
1110        assert!(pred.check(&500));
1111        assert!(pred.check(&1000));
1112        assert!(!pred.check(&1001));
1113    }
1114
1115    #[test]
1116    fn test_predicate_composition() {
1117        // Test AND composition
1118        let and_pred = ge(10_u32).and(le(20_u32));
1119        assert!(and_pred.check(&15));
1120        assert!(!and_pred.check(&5));
1121        assert!(!and_pred.check(&25));
1122
1123        // Test OR composition
1124        let or_pred = lt(10_u32).or(gt(90_u32));
1125        assert!(or_pred.check(&5));
1126        assert!(or_pred.check(&95));
1127        assert!(!or_pred.check(&50));
1128
1129        // Test NOT composition
1130        let not_pred = ge(50_u32).not();
1131        assert!(not_pred.check(&25));
1132        assert!(!not_pred.check(&75));
1133    }
1134
1135    // =========================================================================
1136    // ValidatedFileResults Tests
1137    // =========================================================================
1138
1139    #[test]
1140    fn test_validated_file_results_all_succeeded() {
1141        let metrics = create_test_file_metrics();
1142        let validations = vec![
1143            validation_success(metrics.clone()),
1144            validation_success(metrics.clone()),
1145        ];
1146
1147        let result = ValidatedFileResults::from_validations(validations);
1148
1149        assert!(result.is_all_success());
1150        assert!(!result.has_failures());
1151        assert_eq!(result.succeeded().len(), 2);
1152        assert!(result.failures().is_none());
1153    }
1154
1155    #[test]
1156    fn test_validated_file_results_partial_success() {
1157        let metrics = create_test_file_metrics();
1158        let validations = vec![
1159            validation_success(metrics.clone()),
1160            validation_failure(AnalysisError::parse("Parse error")),
1161            validation_success(metrics.clone()),
1162        ];
1163
1164        let result = ValidatedFileResults::from_validations(validations);
1165
1166        assert!(!result.is_all_success());
1167        assert!(result.has_failures());
1168        assert_eq!(result.succeeded().len(), 2);
1169        assert!(result.failures().is_some());
1170        assert_eq!(result.failures().unwrap().len(), 1);
1171    }
1172
1173    #[test]
1174    fn test_validated_file_results_all_failed() {
1175        let validations: Vec<AnalysisValidation<FileMetrics>> = vec![
1176            validation_failure(AnalysisError::parse("Error 1")),
1177            validation_failure(AnalysisError::parse("Error 2")),
1178        ];
1179
1180        let result = ValidatedFileResults::from_validations(validations);
1181
1182        assert!(!result.is_all_success());
1183        assert!(result.has_failures());
1184        assert!(result.succeeded().is_empty());
1185        assert!(result.failures().is_some());
1186        assert_eq!(result.failures().unwrap().len(), 2);
1187    }
1188
1189    #[test]
1190    fn test_validated_file_results_into_validation() {
1191        let metrics = create_test_file_metrics();
1192
1193        // All success
1194        let all_success = ValidatedFileResults::AllSucceeded(vec![metrics.clone()]);
1195        assert!(all_success.into_validation().is_success());
1196
1197        // Partial success becomes failure
1198        let partial = ValidatedFileResults::PartialSuccess {
1199            succeeded: vec![metrics.clone()],
1200            failures: NonEmptyVec::new(AnalysisError::parse("Error"), Vec::new()),
1201        };
1202        assert!(partial.into_validation().is_failure());
1203    }
1204
1205    #[test]
1206    fn test_validated_file_results_into_lenient_result() {
1207        let metrics = create_test_file_metrics();
1208
1209        // Partial success becomes Ok in lenient mode
1210        let partial = ValidatedFileResults::PartialSuccess {
1211            succeeded: vec![metrics.clone()],
1212            failures: NonEmptyVec::new(AnalysisError::parse("Error"), Vec::new()),
1213        };
1214        let result = partial.into_lenient_result();
1215        assert!(result.is_ok());
1216        assert_eq!(result.unwrap().len(), 1);
1217
1218        // All failed becomes Err even in lenient mode
1219        let all_failed = ValidatedFileResults::AllFailed(NonEmptyVec::new(
1220            AnalysisError::parse("Error"),
1221            vec![],
1222        ));
1223        assert!(all_failed.into_lenient_result().is_err());
1224    }
1225
1226    // =========================================================================
1227    // ValidationRuleSet Tests
1228    // =========================================================================
1229
1230    #[test]
1231    fn test_validation_rule_set_default() {
1232        let rules = ValidationRuleSet::default();
1233        assert_eq!(rules.complexity_warning, 21);
1234        assert_eq!(rules.complexity_critical, 100);
1235        assert_eq!(rules.max_function_length, 50);
1236    }
1237
1238    #[test]
1239    fn test_validation_rule_set_strict() {
1240        let rules = ValidationRuleSet::strict();
1241        assert!(rules.complexity_warning < ValidationRuleSet::default().complexity_warning);
1242        assert!(rules.max_function_length < ValidationRuleSet::default().max_function_length);
1243    }
1244
1245    #[test]
1246    fn test_validation_rule_set_lenient() {
1247        let rules = ValidationRuleSet::lenient();
1248        assert!(rules.complexity_warning > ValidationRuleSet::default().complexity_warning);
1249        assert!(rules.max_function_length > ValidationRuleSet::default().max_function_length);
1250    }
1251
1252    #[test]
1253    fn test_validation_rule_set_checks() {
1254        let rules = ValidationRuleSet::default();
1255
1256        // Complexity checks
1257        assert!(!rules.is_warning_complexity(20));
1258        assert!(rules.is_warning_complexity(50));
1259        assert!(!rules.is_warning_complexity(100));
1260        assert!(!rules.is_critical_complexity(99));
1261        assert!(rules.is_critical_complexity(100));
1262
1263        // Length checks
1264        assert!(rules.is_acceptable_function_length(50));
1265        assert!(!rules.is_acceptable_function_length(51));
1266        assert!(rules.is_acceptable_nesting(4));
1267        assert!(!rules.is_acceptable_nesting(5));
1268    }
1269
1270    #[test]
1271    fn test_validation_rule_set_predicates() {
1272        let rules = ValidationRuleSet::default();
1273
1274        // Complexity predicate
1275        let complexity_pred = rules.complexity_predicate();
1276        assert!(complexity_pred.check(&50));
1277        assert!(!complexity_pred.check(&100));
1278
1279        // Function length predicate
1280        let length_pred = rules.function_length_predicate();
1281        assert!(length_pred.check(&50));
1282        assert!(!length_pred.check(&51));
1283    }
1284
1285    // =========================================================================
1286    // Validation Helper Tests
1287    // =========================================================================
1288
1289    #[test]
1290    fn test_validate_function_complexity() {
1291        let rules = ValidationRuleSet::default();
1292
1293        let valid = validate_function_complexity("test_fn", 50, &rules);
1294        assert!(valid.is_success());
1295
1296        let invalid = validate_function_complexity("complex_fn", 150, &rules);
1297        assert!(invalid.is_failure());
1298    }
1299
1300    #[test]
1301    fn test_validate_function_length() {
1302        let rules = ValidationRuleSet::default();
1303
1304        let valid = validate_function_length("short_fn", 30, &rules);
1305        assert!(valid.is_success());
1306
1307        let invalid = validate_function_length("long_fn", 100, &rules);
1308        assert!(invalid.is_failure());
1309    }
1310
1311    #[test]
1312    fn test_validate_nesting_depth() {
1313        let rules = ValidationRuleSet::default();
1314
1315        let valid = validate_nesting_depth("shallow_fn", 2, &rules);
1316        assert!(valid.is_success());
1317
1318        let invalid = validate_nesting_depth("deep_fn", 10, &rules);
1319        assert!(invalid.is_failure());
1320    }
1321
1322    #[test]
1323    fn test_validate_file_length() {
1324        let rules = ValidationRuleSet::default();
1325        let path = std::path::Path::new("test.rs");
1326
1327        let valid = validate_file_length(path, 500, &rules);
1328        assert!(valid.is_success());
1329
1330        let invalid = validate_file_length(path, 2000, &rules);
1331        assert!(invalid.is_failure());
1332    }
1333
1334    // =========================================================================
1335    // Field Context Tests (Spec 003)
1336    // =========================================================================
1337
1338    #[test]
1339    fn test_field_path_root() {
1340        let path = FieldPath::root();
1341        assert!(path.is_root());
1342        assert!(path.is_empty());
1343        assert_eq!(path.len(), 0);
1344        assert_eq!(path.as_string(), "");
1345    }
1346
1347    #[test]
1348    fn test_field_path_single() {
1349        let path = FieldPath::new("config");
1350        assert!(!path.is_root());
1351        assert_eq!(path.len(), 1);
1352        assert_eq!(path.as_string(), "config");
1353        assert_eq!(path.last(), Some("config"));
1354    }
1355
1356    #[test]
1357    fn test_field_path_nested() {
1358        let path = FieldPath::root()
1359            .push("config")
1360            .push("thresholds")
1361            .push("cyclomatic");
1362        assert_eq!(path.len(), 3);
1363        assert_eq!(path.as_string(), "config.thresholds.cyclomatic");
1364        assert_eq!(path.last(), Some("cyclomatic"));
1365        assert_eq!(path.segments(), &["config", "thresholds", "cyclomatic"]);
1366    }
1367
1368    #[test]
1369    fn test_field_path_display() {
1370        let path = FieldPath::new("config").push("value");
1371        assert_eq!(format!("{}", path), "config.value");
1372    }
1373
1374    #[test]
1375    fn test_field_path_from_str() {
1376        let path: FieldPath = "config".into();
1377        assert_eq!(path.as_string(), "config");
1378    }
1379
1380    #[test]
1381    fn test_validation_error_at_field() {
1382        let path = FieldPath::new("threshold");
1383        let error = ValidationError::at_field(&path, "must be positive");
1384        assert_eq!(error.field.as_string(), "threshold");
1385        assert_eq!(error.message, "must be positive");
1386        assert!(error.expected.is_none());
1387        assert!(error.actual.is_none());
1388    }
1389
1390    #[test]
1391    fn test_validation_error_for_field() {
1392        let error = ValidationError::for_field("coverage", "out of range");
1393        assert_eq!(error.field.as_string(), "coverage");
1394        assert_eq!(error.message, "out of range");
1395    }
1396
1397    #[test]
1398    fn test_validation_error_with_context() {
1399        let error = ValidationError::for_field("threshold", "invalid value")
1400            .with_context("positive integer", "-5");
1401        assert_eq!(error.expected, Some("positive integer".to_string()));
1402        assert_eq!(error.actual, Some("-5".to_string()));
1403    }
1404
1405    #[test]
1406    fn test_validation_error_display() {
1407        let error = ValidationError::for_field("config.threshold", "must be positive")
1408            .with_context("positive", "negative");
1409        let display = format!("{}", error);
1410        assert!(display.contains("config.threshold"));
1411        assert!(display.contains("must be positive"));
1412        assert!(display.contains("expected: positive"));
1413        assert!(display.contains("got: negative"));
1414    }
1415
1416    #[test]
1417    fn test_validation_error_display_no_context() {
1418        let error = ValidationError::for_field("name", "required");
1419        assert_eq!(format!("{}", error), "name: required");
1420    }
1421
1422    #[test]
1423    fn test_validation_error_display_root_path() {
1424        let error = ValidationError::at_field(&FieldPath::root(), "general error");
1425        assert_eq!(format!("{}", error), "general error");
1426    }
1427
1428    #[test]
1429    fn test_validation_error_serialization() {
1430        let error = ValidationError::for_field("threshold", "invalid").with_context(">=0", "-1");
1431        let json = serde_json::to_string(&error).unwrap();
1432        assert!(json.contains("\"field\""));
1433        assert!(json.contains("\"message\""));
1434        assert!(json.contains("\"expected\""));
1435        assert!(json.contains("\"actual\""));
1436    }
1437
1438    #[test]
1439    fn test_file_error_new() {
1440        let error = FileError::new(PathBuf::from("src/main.rs"), "parse error");
1441        assert_eq!(error.path, PathBuf::from("src/main.rs"));
1442        assert_eq!(error.message, "parse error");
1443        assert!(error.line.is_none());
1444        assert!(error.column.is_none());
1445        assert!(error.error_code.is_none());
1446    }
1447
1448    #[test]
1449    fn test_file_error_at_location() {
1450        let error =
1451            FileError::new(PathBuf::from("test.rs"), "unexpected token").at_location(42, 15);
1452        assert_eq!(error.line, Some(42));
1453        assert_eq!(error.column, Some(15));
1454    }
1455
1456    #[test]
1457    fn test_file_error_at_line() {
1458        let error = FileError::new(PathBuf::from("test.rs"), "missing semicolon").at_line(10);
1459        assert_eq!(error.line, Some(10));
1460        assert!(error.column.is_none());
1461    }
1462
1463    #[test]
1464    fn test_file_error_with_code() {
1465        let error = FileError::new(PathBuf::from("test.rs"), "syntax error").with_code("E010");
1466        assert_eq!(error.error_code, Some("E010".to_string()));
1467    }
1468
1469    #[test]
1470    fn test_file_error_display() {
1471        let error = FileError::new(PathBuf::from("src/lib.rs"), "unexpected eof")
1472            .at_location(100, 25)
1473            .with_code("E010");
1474        let display = format!("{}", error);
1475        assert!(display.contains("src/lib.rs"));
1476        assert!(display.contains(":100:25"));
1477        assert!(display.contains("unexpected eof"));
1478        assert!(display.contains("[E010]"));
1479    }
1480
1481    #[test]
1482    fn test_file_error_display_no_location() {
1483        let error = FileError::new(PathBuf::from("test.rs"), "general error");
1484        let display = format!("{}", error);
1485        assert_eq!(display, "test.rs: general error");
1486    }
1487
1488    #[test]
1489    fn test_file_error_serialization() {
1490        let error = FileError::new(PathBuf::from("test.rs"), "error")
1491            .at_location(10, 5)
1492            .with_code("E001");
1493        let json = serde_json::to_string(&error).unwrap();
1494        assert!(json.contains("\"path\""));
1495        assert!(json.contains("\"line\""));
1496        assert!(json.contains("\"column\""));
1497        assert!(json.contains("\"message\""));
1498        assert!(json.contains("\"error_code\""));
1499    }
1500
1501    #[test]
1502    fn test_validated_file_set_empty() {
1503        let set: ValidatedFileSet<String> = ValidatedFileSet::empty();
1504        assert!(!set.has_valid());
1505        assert!(!set.has_errors());
1506        assert!(!set.is_partial_success());
1507        assert!(!set.is_all_success());
1508        assert!(!set.is_all_failed());
1509    }
1510
1511    #[test]
1512    fn test_validated_file_set_all_valid() {
1513        let set = ValidatedFileSet::all_valid(vec!["file1".to_string(), "file2".to_string()]);
1514        assert!(set.has_valid());
1515        assert!(!set.has_errors());
1516        assert!(set.is_all_success());
1517        assert!(!set.is_partial_success());
1518        assert!(!set.is_all_failed());
1519        assert_eq!(set.valid_count(), 2);
1520        assert_eq!(set.error_count(), 0);
1521    }
1522
1523    #[test]
1524    fn test_validated_file_set_all_errors() {
1525        let set: ValidatedFileSet<String> = ValidatedFileSet::all_errors(vec![
1526            FileError::new("a.rs", "error1"),
1527            FileError::new("b.rs", "error2"),
1528        ]);
1529        assert!(!set.has_valid());
1530        assert!(set.has_errors());
1531        assert!(set.is_all_failed());
1532        assert!(!set.is_partial_success());
1533        assert!(!set.is_all_success());
1534        assert_eq!(set.valid_count(), 0);
1535        assert_eq!(set.error_count(), 2);
1536    }
1537
1538    #[test]
1539    fn test_validated_file_set_partial_success() {
1540        let set = ValidatedFileSet {
1541            valid: vec!["good.rs".to_string()],
1542            errors: vec![FileError::new("bad.rs", "parse error")],
1543        };
1544        assert!(set.has_valid());
1545        assert!(set.has_errors());
1546        assert!(set.is_partial_success());
1547        assert!(!set.is_all_success());
1548        assert!(!set.is_all_failed());
1549    }
1550
1551    #[test]
1552    fn test_validated_file_set_into_strict_result() {
1553        let success_set = ValidatedFileSet::all_valid(vec!["ok".to_string()]);
1554        assert!(success_set.into_strict_result().is_ok());
1555
1556        let partial_set = ValidatedFileSet {
1557            valid: vec!["ok".to_string()],
1558            errors: vec![FileError::new("bad.rs", "error")],
1559        };
1560        assert!(partial_set.into_strict_result().is_err());
1561    }
1562
1563    #[test]
1564    fn test_validated_file_set_into_lenient_result() {
1565        let partial_set = ValidatedFileSet {
1566            valid: vec!["ok".to_string()],
1567            errors: vec![FileError::new("bad.rs", "error")],
1568        };
1569        assert!(partial_set.into_lenient_result().is_ok());
1570
1571        let all_failed: ValidatedFileSet<String> =
1572            ValidatedFileSet::all_errors(vec![FileError::new("bad.rs", "error")]);
1573        assert!(all_failed.into_lenient_result().is_err());
1574    }
1575
1576    #[test]
1577    fn test_validated_file_set_add_operations() {
1578        let mut set: ValidatedFileSet<String> = ValidatedFileSet::empty();
1579        set.add_valid("file1".to_string());
1580        set.add_error(FileError::new("bad.rs", "error"));
1581        assert!(set.is_partial_success());
1582        assert_eq!(set.valid_count(), 1);
1583        assert_eq!(set.error_count(), 1);
1584    }
1585
1586    #[test]
1587    fn test_validated_file_set_merge() {
1588        let mut set1: ValidatedFileSet<String> = ValidatedFileSet::all_valid(vec!["a".to_string()]);
1589        let set2 = ValidatedFileSet {
1590            valid: vec!["b".to_string()],
1591            errors: vec![FileError::new("c.rs", "error")],
1592        };
1593        set1.merge(set2);
1594        assert_eq!(set1.valid_count(), 2);
1595        assert_eq!(set1.error_count(), 1);
1596    }
1597
1598    #[test]
1599    fn test_validated_file_set_serialization() {
1600        let set = ValidatedFileSet {
1601            valid: vec!["file1".to_string()],
1602            errors: vec![FileError::new("bad.rs", "error")],
1603        };
1604        let json = serde_json::to_string(&set).unwrap();
1605        assert!(json.contains("\"valid_count\":1"));
1606        assert!(json.contains("\"error_count\":1"));
1607        assert!(json.contains("\"valid\""));
1608        assert!(json.contains("\"errors\""));
1609    }
1610
1611    #[test]
1612    fn test_field_context_ext_with_field_path() {
1613        let validation: Validation<u32, NonEmptyVec<String>> =
1614            Validation::Failure(NonEmptyVec::new("error message".to_string(), vec![]));
1615
1616        let path = FieldPath::new("config").push("threshold");
1617        let result = validation.with_field_path(&path);
1618
1619        match result {
1620            Validation::Failure(errors) => {
1621                let err = errors.head();
1622                assert_eq!(err.field.as_string(), "config.threshold");
1623                assert!(err.message.contains("error message"));
1624            }
1625            _ => panic!("Expected failure"),
1626        }
1627    }
1628
1629    #[test]
1630    fn test_field_context_ext_with_field_name() {
1631        let validation: Validation<u32, NonEmptyVec<String>> =
1632            Validation::Failure(NonEmptyVec::new("too large".to_string(), vec![]));
1633
1634        let result = validation.with_field_name("complexity");
1635
1636        match result {
1637            Validation::Failure(errors) => {
1638                let err = errors.head();
1639                assert_eq!(err.field.as_string(), "complexity");
1640            }
1641            _ => panic!("Expected failure"),
1642        }
1643    }
1644
1645    #[test]
1646    fn test_field_context_ext_success_passthrough() {
1647        let validation: Validation<u32, NonEmptyVec<String>> = Validation::Success(42);
1648        let result = validation.with_field_name("value");
1649
1650        match result {
1651            Validation::Success(v) => assert_eq!(v, 42),
1652            _ => panic!("Expected success"),
1653        }
1654    }
1655
1656    #[test]
1657    fn test_validate_field_with_stillwater() {
1658        // Test that we can use stillwater's with_field via our wrapper
1659        let validation: Validation<u32, String> = Validation::Failure("test error".to_string());
1660        let result = validate_field("my_field", validation);
1661
1662        match result {
1663            Validation::Failure(field_error) => {
1664                assert_eq!(field_error.field, "my_field");
1665                assert_eq!(field_error.error, "test error");
1666            }
1667            _ => panic!("Expected failure"),
1668        }
1669    }
1670
1671    // =========================================================================
1672    // Helper Functions
1673    // =========================================================================
1674
1675    fn create_test_file_metrics() -> FileMetrics {
1676        use crate::core::{ComplexityMetrics, Language};
1677        FileMetrics {
1678            path: std::path::PathBuf::from("test.rs"),
1679            language: Language::Rust,
1680            complexity: ComplexityMetrics::default(),
1681            debt_items: Vec::new(),
1682            dependencies: Vec::new(),
1683            duplications: Vec::new(),
1684            total_lines: 100,
1685            module_scope: None,
1686            classes: None,
1687        }
1688    }
1689}