dx-driven 0.1.0

Professional AI-assisted development orchestrator with binary-first architecture
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
//! Configuration Validation Module
//!
//! This module provides comprehensive validation for all configuration sections
//! with actionable error messages and suggestions.
//!
//! ## Features
//!
//! - Validates all config sections ([driven], [generator], [dcp])
//! - Provides suggestions for invalid configurations
//! - Supports partial validation for incremental updates

use crate::{DrivenConfig, DrivenError, Result};
use std::collections::HashSet;
use std::path::Path;

/// Configuration validator
#[derive(Debug, Default)]
pub struct ConfigValidator {
    /// Validation errors collected
    errors: Vec<ValidationError>,
    /// Validation warnings collected
    warnings: Vec<ValidationWarning>,
}

/// A validation error
#[derive(Debug, Clone)]
pub struct ValidationError {
    /// Field path (e.g., "sync.debounce_ms")
    pub field: String,
    /// Error message
    pub message: String,
    /// Suggestion for fixing
    pub suggestion: String,
}

/// A validation warning
#[derive(Debug, Clone)]
pub struct ValidationWarning {
    /// Field path
    pub field: String,
    /// Warning message
    pub message: String,
    /// Suggestion
    pub suggestion: String,
}

/// Validation result
#[derive(Debug)]
pub struct ValidationReport {
    /// Whether validation passed
    pub valid: bool,
    /// Errors found
    pub errors: Vec<ValidationError>,
    /// Warnings found
    pub warnings: Vec<ValidationWarning>,
}

impl ValidationReport {
    /// Check if there are any errors
    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }

    /// Check if there are any warnings
    pub fn has_warnings(&self) -> bool {
        !self.warnings.is_empty()
    }

    /// Get a formatted error message
    pub fn format_errors(&self) -> String {
        self.errors
            .iter()
            .map(|e| {
                format!(
                    "  - {}: {}\n    Suggestion: {}",
                    e.field, e.message, e.suggestion
                )
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Get a formatted warning message
    pub fn format_warnings(&self) -> String {
        self.warnings
            .iter()
            .map(|w| {
                format!(
                    "  - {}: {}\n    Suggestion: {}",
                    w.field, w.message, w.suggestion
                )
            })
            .collect::<Vec<_>>()
            .join("\n")
    }
}

impl ConfigValidator {
    /// Create a new validator
    pub fn new() -> Self {
        Self::default()
    }

    /// Validate a complete DrivenConfig
    pub fn validate(&mut self, config: &DrivenConfig) -> ValidationReport {
        self.errors.clear();
        self.warnings.clear();

        self.validate_version(&config.version);
        self.validate_editors(&config.editors);
        self.validate_sync(&config.sync);
        self.validate_templates(&config.templates);
        self.validate_context(&config.context);

        ValidationReport {
            valid: self.errors.is_empty(),
            errors: self.errors.clone(),
            warnings: self.warnings.clone(),
        }
    }

    /// Validate version field
    fn validate_version(&mut self, version: &str) {
        if version.is_empty() {
            self.errors.push(ValidationError {
                field: "version".to_string(),
                message: "Version cannot be empty".to_string(),
                suggestion: "Set version to '1.0'".to_string(),
            });
        } else if !version.chars().all(|c| c.is_ascii_digit() || c == '.') {
            self.warnings.push(ValidationWarning {
                field: "version".to_string(),
                message: format!("Unusual version format: '{}'", version),
                suggestion: "Use semantic versioning (e.g., '1.0', '2.1')".to_string(),
            });
        }
    }

    /// Validate editor configuration
    fn validate_editors(&mut self, editors: &crate::EditorConfig) {
        let enabled_count = [
            editors.cursor,
            editors.copilot,
            editors.windsurf,
            editors.claude,
            editors.aider,
            editors.cline,
        ]
        .iter()
        .filter(|&&e| e)
        .count();

        if enabled_count == 0 {
            self.warnings.push(ValidationWarning {
                field: "editors".to_string(),
                message: "No editors are enabled".to_string(),
                suggestion: "Enable at least one editor (e.g., editors.cursor = true)".to_string(),
            });
        }
    }

    /// Validate sync configuration
    fn validate_sync(&mut self, sync: &crate::SyncConfig) {
        // Validate source of truth path
        if sync.source_of_truth.is_empty() {
            self.errors.push(ValidationError {
                field: "sync.source_of_truth".to_string(),
                message: "Source of truth path cannot be empty".to_string(),
                suggestion: "Set to '.driven/rules.drv' or another valid path".to_string(),
            });
        } else if !sync.source_of_truth.ends_with(".drv") && !sync.source_of_truth.ends_with(".md")
        {
            self.warnings.push(ValidationWarning {
                field: "sync.source_of_truth".to_string(),
                message: format!("Unusual file extension: '{}'", sync.source_of_truth),
                suggestion: "Use .drv for binary format or .md for markdown".to_string(),
            });
        }
    }

    /// Validate template configuration
    fn validate_templates(&mut self, templates: &crate::TemplateConfig) {
        // Check for duplicate personas
        let mut seen = HashSet::new();
        for persona in &templates.personas {
            if !seen.insert(persona) {
                self.warnings.push(ValidationWarning {
                    field: "templates.personas".to_string(),
                    message: format!("Duplicate persona: '{}'", persona),
                    suggestion: "Remove duplicate entries".to_string(),
                });
            }
        }

        // Check for duplicate standards
        seen.clear();
        for standard in &templates.standards {
            if !seen.insert(standard) {
                self.warnings.push(ValidationWarning {
                    field: "templates.standards".to_string(),
                    message: format!("Duplicate standard: '{}'", standard),
                    suggestion: "Remove duplicate entries".to_string(),
                });
            }
        }
    }

    /// Validate context configuration
    fn validate_context(&mut self, context: &crate::ContextConfig) {
        // Check for empty include patterns
        if context.include.is_empty() {
            self.warnings.push(ValidationWarning {
                field: "context.include".to_string(),
                message: "No include patterns specified".to_string(),
                suggestion: "Add patterns like 'src/**' to include source files".to_string(),
            });
        }

        // Check for overlapping include/exclude patterns
        for include in &context.include {
            for exclude in &context.exclude {
                if include == exclude {
                    self.errors.push(ValidationError {
                        field: "context".to_string(),
                        message: format!("Pattern '{}' is both included and excluded", include),
                        suggestion: "Remove the pattern from either include or exclude".to_string(),
                    });
                }
            }
        }

        // Validate index path
        if context.index_path.is_empty() {
            self.errors.push(ValidationError {
                field: "context.index_path".to_string(),
                message: "Index path cannot be empty".to_string(),
                suggestion: "Set to '.driven/index.drv'".to_string(),
            });
        }
    }

    /// Validate a file path exists
    pub fn validate_path_exists(&mut self, field: &str, path: &Path) {
        if !path.exists() {
            self.errors.push(ValidationError {
                field: field.to_string(),
                message: format!("Path does not exist: {}", path.display()),
                suggestion: "Create the file or update the path".to_string(),
            });
        }
    }

    /// Add a custom error
    pub fn add_error(&mut self, field: &str, message: &str, suggestion: &str) {
        self.errors.push(ValidationError {
            field: field.to_string(),
            message: message.to_string(),
            suggestion: suggestion.to_string(),
        });
    }

    /// Add a custom warning
    pub fn add_warning(&mut self, field: &str, message: &str, suggestion: &str) {
        self.warnings.push(ValidationWarning {
            field: field.to_string(),
            message: message.to_string(),
            suggestion: suggestion.to_string(),
        });
    }
}

/// Validate a configuration and return a Result
pub fn validate_config(config: &DrivenConfig) -> Result<ValidationReport> {
    let mut validator = ConfigValidator::new();
    let report = validator.validate(config);

    if report.has_errors() {
        Err(DrivenError::Validation(report.format_errors()))
    } else {
        Ok(report)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::EditorConfig;

    #[test]
    fn test_valid_config() {
        let config = DrivenConfig::default();
        let mut validator = ConfigValidator::new();
        let report = validator.validate(&config);

        assert!(report.valid);
        assert!(!report.has_errors());
    }

    #[test]
    fn test_empty_version() {
        let mut config = DrivenConfig::default();
        config.version = String::new();

        let mut validator = ConfigValidator::new();
        let report = validator.validate(&config);

        assert!(!report.valid);
        assert!(report.errors.iter().any(|e| e.field == "version"));
    }

    #[test]
    fn test_no_editors_enabled() {
        let mut config = DrivenConfig::default();
        config.editors = EditorConfig {
            cursor: false,
            copilot: false,
            windsurf: false,
            claude: false,
            aider: false,
            cline: false,
        };

        let mut validator = ConfigValidator::new();
        let report = validator.validate(&config);

        assert!(report.has_warnings());
        assert!(report.warnings.iter().any(|w| w.field == "editors"));
    }

    #[test]
    fn test_empty_source_of_truth() {
        let mut config = DrivenConfig::default();
        config.sync.source_of_truth = String::new();

        let mut validator = ConfigValidator::new();
        let report = validator.validate(&config);

        assert!(!report.valid);
        assert!(
            report
                .errors
                .iter()
                .any(|e| e.field == "sync.source_of_truth")
        );
    }

    #[test]
    fn test_duplicate_personas() {
        let mut config = DrivenConfig::default();
        config.templates.personas = vec!["architect".to_string(), "architect".to_string()];

        let mut validator = ConfigValidator::new();
        let report = validator.validate(&config);

        assert!(report.has_warnings());
        assert!(
            report
                .warnings
                .iter()
                .any(|w| w.message.contains("Duplicate"))
        );
    }

    #[test]
    fn test_overlapping_patterns() {
        let mut config = DrivenConfig::default();
        config.context.include = vec!["src/**".to_string()];
        config.context.exclude = vec!["src/**".to_string()];

        let mut validator = ConfigValidator::new();
        let report = validator.validate(&config);

        assert!(!report.valid);
        assert!(
            report
                .errors
                .iter()
                .any(|e| e.message.contains("both included and excluded"))
        );
    }

    #[test]
    fn test_validation_report_formatting() {
        let report = ValidationReport {
            valid: false,
            errors: vec![ValidationError {
                field: "test.field".to_string(),
                message: "Test error".to_string(),
                suggestion: "Fix it".to_string(),
            }],
            warnings: vec![ValidationWarning {
                field: "test.warning".to_string(),
                message: "Test warning".to_string(),
                suggestion: "Consider fixing".to_string(),
            }],
        };

        let errors = report.format_errors();
        assert!(errors.contains("test.field"));
        assert!(errors.contains("Test error"));

        let warnings = report.format_warnings();
        assert!(warnings.contains("test.warning"));
        assert!(warnings.contains("Test warning"));
    }
}

#[cfg(test)]
mod property_tests {
    use super::*;
    use proptest::prelude::*;

    // Property 12: Configuration Validation
    // For any configuration input, validation SHALL correctly identify all
    // invalid settings and provide actionable error messages.

    proptest! {
        /// Property: Valid default config always passes validation
        #[test]
        fn prop_default_config_valid(_seed in any::<u64>()) {
            let config = DrivenConfig::default();
            let mut validator = ConfigValidator::new();
            let report = validator.validate(&config);

            prop_assert!(report.valid, "Default config should be valid");
        }

        /// Property: Empty version always fails validation
        #[test]
        fn prop_empty_version_invalid(_seed in any::<u64>()) {
            let mut config = DrivenConfig::default();
            config.version = String::new();

            let mut validator = ConfigValidator::new();
            let report = validator.validate(&config);

            prop_assert!(!report.valid);
            prop_assert!(report.errors.iter().any(|e| e.field == "version"));
        }

        /// Property: Empty source_of_truth always fails validation
        #[test]
        fn prop_empty_source_of_truth_invalid(_seed in any::<u64>()) {
            let mut config = DrivenConfig::default();
            config.sync.source_of_truth = String::new();

            let mut validator = ConfigValidator::new();
            let report = validator.validate(&config);

            prop_assert!(!report.valid);
            prop_assert!(report.errors.iter().any(|e| e.field.contains("source_of_truth")));
        }

        /// Property: All errors have suggestions
        #[test]
        fn prop_errors_have_suggestions(
            version in ".*",
            source in ".*",
        ) {
            let mut config = DrivenConfig::default();
            config.version = version;
            config.sync.source_of_truth = source;

            let mut validator = ConfigValidator::new();
            let report = validator.validate(&config);

            for error in &report.errors {
                prop_assert!(!error.suggestion.is_empty(),
                    "Error for '{}' should have a suggestion", error.field);
            }

            for warning in &report.warnings {
                prop_assert!(!warning.suggestion.is_empty(),
                    "Warning for '{}' should have a suggestion", warning.field);
            }
        }

        /// Property: Validation is deterministic
        #[test]
        fn prop_validation_deterministic(
            version in "[0-9.]+",
        ) {
            let mut config = DrivenConfig::default();
            config.version = version;

            let mut validator1 = ConfigValidator::new();
            let report1 = validator1.validate(&config);

            let mut validator2 = ConfigValidator::new();
            let report2 = validator2.validate(&config);

            prop_assert_eq!(report1.valid, report2.valid);
            prop_assert_eq!(report1.errors.len(), report2.errors.len());
            prop_assert_eq!(report1.warnings.len(), report2.warnings.len());
        }
    }
}