clnrm_core/config/
weaver.rs

1//! Weaver live-check configuration
2//!
3//! Configuration for Weaver live-checking support in TOML files.
4//! Supports comprehensive validation modes including strict, lenient, and 80/20 patterns.
5
6use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10/// Weaver live-check configuration (v1.3.0)
11///
12/// Enables Weaver validation for test execution when present in TOML.
13/// Supports multiple validation modes and comprehensive telemetry coverage tracking.
14#[derive(Debug, Deserialize, Serialize, Clone)]
15pub struct WeaverConfig {
16    /// Enable Weaver live-checking
17    /// Default: true if [weaver] section is present
18    #[serde(default = "default_true")]
19    pub enabled: bool,
20
21    /// Registry path for Weaver schemas
22    /// Default: "registry" (relative to installation)
23    #[serde(default = "default_registry_path")]
24    pub registry_path: String,
25
26    /// OTLP gRPC port (0 = auto-discover)
27    /// Default: 0 (auto-discover)
28    /// Range: 0 or 1024-65535
29    #[serde(default)]
30    pub otlp_port: u16,
31
32    /// Admin port for control interface (0 = auto-discover)
33    /// Default: 0 (auto-discover)
34    /// Range: 0 or 1024-65535
35    #[serde(default)]
36    pub admin_port: u16,
37
38    /// Output directory for validation reports
39    /// Default: "./validation_output"
40    #[serde(default = "default_output_dir")]
41    pub output_dir: String,
42
43    /// Enable streaming output (real-time feedback)
44    /// Default: false
45    #[serde(default)]
46    pub stream: bool,
47
48    /// Fail fast on first violation
49    /// Default: false
50    #[serde(default)]
51    pub fail_fast: bool,
52
53    /// Validation configuration
54    #[serde(default)]
55    pub validation: Option<ValidationConfig>,
56
57    /// 80/20 validation mode configuration
58    #[serde(default, rename = "80_20")]
59    pub eighty_twenty: Option<EightyTwentyConfig>,
60
61    /// Collector management configuration
62    #[serde(default)]
63    pub collector: Option<CollectorConfig>,
64
65    /// Report generation configuration
66    #[serde(default)]
67    pub reports: Option<ReportsConfig>,
68
69    /// Performance tuning configuration
70    #[serde(default)]
71    pub performance: Option<PerformanceConfig>,
72}
73
74/// Validation mode configuration
75#[derive(Debug, Deserialize, Serialize, Clone)]
76pub struct ValidationConfig {
77    /// Validation strictness mode
78    /// Options: strict (100%), lenient (90%), 80_20 (80%), minimal (60%)
79    #[serde(default = "default_strict_mode")]
80    pub mode: ValidationMode,
81
82    /// Fail test execution on schema violations
83    /// Default: true
84    #[serde(default = "default_true")]
85    pub fail_on_violation: bool,
86
87    /// Fail on missing optional attributes
88    /// Default: false
89    #[serde(default)]
90    pub fail_on_missing_optional: bool,
91
92    /// Coverage threshold (percentage of expected telemetry observed)
93    /// Range: 0.0-100.0
94    /// Default: 80.0
95    #[serde(default = "default_coverage_threshold")]
96    pub coverage_threshold: f64,
97
98    /// Timeout for waiting for telemetry after test completes (seconds)
99    /// Default: 5
100    #[serde(default = "default_inactivity_timeout")]
101    pub inactivity_timeout: u64,
102
103    /// Diagnostic output format
104    /// Options: ansi, json, gh_workflow_command, auto
105    #[serde(default = "default_diagnostic_format")]
106    pub diagnostic_format: DiagnosticFormat,
107}
108
109/// Validation strictness mode
110#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
111#[serde(rename_all = "snake_case")]
112pub enum ValidationMode {
113    /// Strict mode: All schema violations fail (100% coverage)
114    Strict,
115    /// Lenient mode: Only critical violations fail (90% coverage)
116    Lenient,
117    /// 80/20 mode: Focus on critical 20% of telemetry (80% coverage)
118    #[serde(rename = "80_20")]
119    EightyTwenty,
120    /// Minimal mode: Basic validation only (60% coverage)
121    Minimal,
122}
123
124/// Diagnostic output format
125#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
126#[serde(rename_all = "snake_case")]
127pub enum DiagnosticFormat {
128    /// Colored terminal output (ANSI escape codes)
129    Ansi,
130    /// Machine-readable JSON output
131    Json,
132    /// GitHub Actions workflow commands (annotations)
133    GhWorkflowCommand,
134    /// Auto-detect based on environment
135    Auto,
136}
137
138/// 80/20 validation mode configuration
139///
140/// Focuses validation on the critical 20% of telemetry that provides 80% of value.
141#[derive(Debug, Deserialize, Serialize, Clone)]
142pub struct EightyTwentyConfig {
143    /// Enable 80/20 validation mode
144    /// Default: false (only applies when validation.mode = "80_20")
145    #[serde(default)]
146    pub enabled: bool,
147
148    /// Critical spans that MUST be present
149    /// Example: ["clnrm.test.execute", "clnrm.container.start"]
150    #[serde(default)]
151    pub critical_spans: Vec<String>,
152
153    /// Required attributes that MUST be present on all spans
154    /// Example: ["clnrm.version", "test.hermetic"]
155    #[serde(default)]
156    pub required_attributes: Vec<String>,
157
158    /// Optional attributes (counted for coverage, but don't fail if missing)
159    /// Example: ["service.name", "container.image"]
160    #[serde(default)]
161    pub optional_attributes: Vec<String>,
162
163    /// Minimum critical span coverage (percentage)
164    /// Default: 100.0 (all critical spans must be present)
165    #[serde(default = "default_full_coverage")]
166    pub critical_span_coverage: f64,
167
168    /// Minimum required attribute coverage (percentage)
169    /// Default: 100.0 (all required attributes must be present)
170    #[serde(default = "default_full_coverage")]
171    pub required_attribute_coverage: f64,
172
173    /// Minimum optional attribute coverage (percentage)
174    /// Default: 50.0 (at least 50% of optional attributes should be present)
175    #[serde(default = "default_half_coverage")]
176    pub optional_attribute_coverage: f64,
177}
178
179/// Collector management configuration
180#[derive(Debug, Deserialize, Serialize, Clone)]
181pub struct CollectorConfig {
182    /// Use existing OTEL collector instead of starting new one
183    /// Default: false
184    #[serde(default)]
185    pub use_existing: bool,
186
187    /// Existing collector endpoint (only if use_existing = true)
188    /// Example: "http://localhost:4317"
189    #[serde(default)]
190    pub endpoint: Option<String>,
191
192    /// Auto-start collector if not already running
193    /// Default: true
194    #[serde(default = "default_true")]
195    pub auto_start: bool,
196
197    /// Docker image for collector (if auto_start = true)
198    /// Default: "otel/opentelemetry-collector:latest"
199    #[serde(default = "default_collector_image")]
200    pub image: String,
201
202    /// Health check timeout (seconds)
203    /// Default: 30
204    #[serde(default = "default_health_check_timeout")]
205    pub health_check_timeout: u64,
206
207    /// Collector startup grace period (seconds)
208    /// Default: 2
209    #[serde(default = "default_startup_grace_period")]
210    pub startup_grace_period: u64,
211}
212
213/// Report generation configuration
214#[derive(Debug, Deserialize, Serialize, Clone)]
215pub struct ReportsConfig {
216    /// Generate JSON validation report
217    /// Default: true
218    #[serde(default = "default_true")]
219    pub json_report: bool,
220
221    /// JSON report filename
222    /// Default: "validation_report.json"
223    #[serde(default = "default_json_report_file")]
224    pub json_report_file: String,
225
226    /// Generate HTML visualization report
227    /// Default: false
228    #[serde(default)]
229    pub html_report: bool,
230
231    /// HTML report filename
232    /// Default: "validation_report.html"
233    #[serde(default = "default_html_report_file")]
234    pub html_report_file: String,
235
236    /// Generate JUnit XML report (for CI/CD)
237    /// Default: false
238    #[serde(default)]
239    pub junit_report: bool,
240
241    /// JUnit XML filename
242    /// Default: "weaver_validation.xml"
243    #[serde(default = "default_junit_report_file")]
244    pub junit_report_file: String,
245
246    /// Include telemetry samples in reports
247    /// Default: true
248    #[serde(default = "default_true")]
249    pub include_samples: bool,
250
251    /// Maximum samples per violation
252    /// Default: 3
253    #[serde(default = "default_max_samples")]
254    pub max_samples_per_violation: u32,
255}
256
257/// Performance tuning configuration
258#[derive(Debug, Deserialize, Serialize, Clone)]
259pub struct PerformanceConfig {
260    /// Buffer size for telemetry ingestion (bytes)
261    /// Default: 1048576 (1MB)
262    #[serde(default = "default_buffer_size")]
263    pub buffer_size: u64,
264
265    /// Maximum concurrent validation workers
266    /// Default: 4
267    #[serde(default = "default_max_workers")]
268    pub max_workers: u32,
269
270    /// Enable telemetry batching
271    /// Default: true
272    #[serde(default = "default_true")]
273    pub batching: bool,
274
275    /// Batch size (number of telemetry items)
276    /// Default: 100
277    #[serde(default = "default_batch_size")]
278    pub batch_size: u32,
279
280    /// Batch timeout (milliseconds)
281    /// Default: 1000
282    #[serde(default = "default_batch_timeout")]
283    pub batch_timeout_ms: u64,
284}
285
286// ============================================================================
287// Default value functions
288// ============================================================================
289
290fn default_true() -> bool {
291    true
292}
293
294fn default_registry_path() -> String {
295    "registry".to_string()
296}
297
298fn default_output_dir() -> String {
299    "./validation_output".to_string()
300}
301
302fn default_strict_mode() -> ValidationMode {
303    ValidationMode::Strict
304}
305
306fn default_coverage_threshold() -> f64 {
307    80.0
308}
309
310fn default_inactivity_timeout() -> u64 {
311    5
312}
313
314fn default_diagnostic_format() -> DiagnosticFormat {
315    DiagnosticFormat::Ansi
316}
317
318fn default_full_coverage() -> f64 {
319    100.0
320}
321
322fn default_half_coverage() -> f64 {
323    50.0
324}
325
326fn default_collector_image() -> String {
327    "otel/opentelemetry-collector:latest".to_string()
328}
329
330fn default_health_check_timeout() -> u64 {
331    30
332}
333
334fn default_startup_grace_period() -> u64 {
335    2
336}
337
338fn default_json_report_file() -> String {
339    "validation_report.json".to_string()
340}
341
342fn default_html_report_file() -> String {
343    "validation_report.html".to_string()
344}
345
346fn default_junit_report_file() -> String {
347    "weaver_validation.xml".to_string()
348}
349
350fn default_max_samples() -> u32 {
351    3
352}
353
354fn default_buffer_size() -> u64 {
355    1048576 // 1MB
356}
357
358fn default_max_workers() -> u32 {
359    4
360}
361
362fn default_batch_size() -> u32 {
363    100
364}
365
366fn default_batch_timeout() -> u64 {
367    1000
368}
369
370// ============================================================================
371// Implementations
372// ============================================================================
373
374impl Default for WeaverConfig {
375    fn default() -> Self {
376        Self {
377            enabled: true,
378            registry_path: "registry".to_string(),
379            otlp_port: 0,
380            admin_port: 0,
381            output_dir: "./validation_output".to_string(),
382            stream: false,
383            fail_fast: false,
384            validation: Some(ValidationConfig::default()),
385            eighty_twenty: None,
386            collector: Some(CollectorConfig::default()),
387            reports: Some(ReportsConfig::default()),
388            performance: Some(PerformanceConfig::default()),
389        }
390    }
391}
392
393impl Default for ValidationConfig {
394    fn default() -> Self {
395        Self {
396            mode: ValidationMode::Strict,
397            fail_on_violation: true,
398            fail_on_missing_optional: false,
399            coverage_threshold: 80.0,
400            inactivity_timeout: 5,
401            diagnostic_format: DiagnosticFormat::Ansi,
402        }
403    }
404}
405
406impl Default for EightyTwentyConfig {
407    fn default() -> Self {
408        Self {
409            enabled: false,
410            critical_spans: Vec::new(),
411            required_attributes: Vec::new(),
412            optional_attributes: Vec::new(),
413            critical_span_coverage: 100.0,
414            required_attribute_coverage: 100.0,
415            optional_attribute_coverage: 50.0,
416        }
417    }
418}
419
420impl Default for CollectorConfig {
421    fn default() -> Self {
422        Self {
423            use_existing: false,
424            endpoint: None,
425            auto_start: true,
426            image: "otel/opentelemetry-collector:latest".to_string(),
427            health_check_timeout: 30,
428            startup_grace_period: 2,
429        }
430    }
431}
432
433impl Default for ReportsConfig {
434    fn default() -> Self {
435        Self {
436            json_report: true,
437            json_report_file: "validation_report.json".to_string(),
438            html_report: false,
439            html_report_file: "validation_report.html".to_string(),
440            junit_report: false,
441            junit_report_file: "weaver_validation.xml".to_string(),
442            include_samples: true,
443            max_samples_per_violation: 3,
444        }
445    }
446}
447
448impl Default for PerformanceConfig {
449    fn default() -> Self {
450        Self {
451            buffer_size: 1048576,
452            max_workers: 4,
453            batching: true,
454            batch_size: 100,
455            batch_timeout_ms: 1000,
456        }
457    }
458}
459
460impl WeaverConfig {
461    /// Validate configuration for consistency and correctness
462    pub fn validate(&self) -> Result<()> {
463        // Validate port ranges
464        if self.otlp_port > 0 && self.otlp_port < 1024 {
465            return Err(CleanroomError::validation_error(
466                "OTLP port must be >= 1024 or 0 for auto-discovery",
467            ));
468        }
469
470        if self.admin_port > 0 && self.admin_port < 1024 {
471            return Err(CleanroomError::validation_error(
472                "Admin port must be >= 1024 or 0 for auto-discovery",
473            ));
474        }
475
476        // Validate ports are different if both specified
477        if self.otlp_port == self.admin_port && self.otlp_port > 0 {
478            return Err(CleanroomError::validation_error(
479                "OTLP port and admin port must be different",
480            ));
481        }
482
483        // Validate validation config if present
484        if let Some(ref validation) = self.validation {
485            validation.validate()?;
486        }
487
488        // Validate 80/20 config if present
489        if let Some(ref eighty_twenty) = self.eighty_twenty {
490            eighty_twenty.validate()?;
491        }
492
493        // Check that 80/20 mode has required config
494        if let Some(ref validation) = self.validation {
495            if validation.mode == ValidationMode::EightyTwenty && self.eighty_twenty.is_none() {
496                return Err(CleanroomError::validation_error(
497                    "80/20 validation mode requires [weaver.80_20] section with critical_spans and required_attributes",
498                ));
499            }
500        }
501
502        // Validate collector config if present
503        if let Some(ref collector) = self.collector {
504            collector.validate()?;
505        }
506
507        Ok(())
508    }
509
510    /// Convert to telemetry::WeaverConfig (for backward compatibility)
511    pub fn to_telemetry_config(&self) -> Result<crate::telemetry::weaver_controller::WeaverConfig> {
512        use crate::telemetry::weaver_controller::WeaverConfig as TelemetryWeaverConfig;
513
514        // Resolve registry path (can be relative or absolute)
515        let registry_path =
516            if self.registry_path.starts_with('/') || self.registry_path.starts_with("~/") {
517                // Absolute path or home directory
518                PathBuf::from(self.registry_path.replace(
519                    "~/",
520                    &format!(
521                        "{}/",
522                        std::env::var("HOME").unwrap_or_else(|_| ".".to_string())
523                    ),
524                ))
525            } else {
526                // Relative path - resolve from installation directory or current directory
527                // Note: Actual resolution happens in run command
528                PathBuf::from(&self.registry_path)
529            };
530
531        Ok(TelemetryWeaverConfig {
532            registry_path,
533            otlp_port: self.otlp_port,
534            admin_port: self.admin_port,
535            output_dir: PathBuf::from(&self.output_dir),
536            stream: self.stream,
537        })
538    }
539}
540
541impl ValidationConfig {
542    /// Validate validation configuration
543    pub fn validate(&self) -> Result<()> {
544        // Check coverage threshold range
545        if !(0.0..=100.0).contains(&self.coverage_threshold) {
546            return Err(CleanroomError::validation_error(format!(
547                "Coverage threshold must be between 0.0 and 100.0, got {}",
548                self.coverage_threshold
549            )));
550        }
551
552        // Check inactivity timeout is reasonable
553        if self.inactivity_timeout == 0 {
554            return Err(CleanroomError::validation_error(
555                "Inactivity timeout must be greater than 0 seconds",
556            ));
557        }
558
559        Ok(())
560    }
561}
562
563impl EightyTwentyConfig {
564    /// Validate 80/20 configuration
565    pub fn validate(&self) -> Result<()> {
566        // If enabled, must have critical spans defined
567        if self.enabled && self.critical_spans.is_empty() {
568            return Err(CleanroomError::validation_error(
569                "80/20 mode requires at least one critical span in critical_spans list",
570            ));
571        }
572
573        // If enabled, must have required attributes defined
574        if self.enabled && self.required_attributes.is_empty() {
575            return Err(CleanroomError::validation_error(
576                "80/20 mode requires at least one required attribute in required_attributes list",
577            ));
578        }
579
580        // Validate coverage thresholds
581        if !(0.0..=100.0).contains(&self.critical_span_coverage) {
582            return Err(CleanroomError::validation_error(format!(
583                "Critical span coverage must be between 0.0 and 100.0, got {}",
584                self.critical_span_coverage
585            )));
586        }
587
588        if !(0.0..=100.0).contains(&self.required_attribute_coverage) {
589            return Err(CleanroomError::validation_error(format!(
590                "Required attribute coverage must be between 0.0 and 100.0, got {}",
591                self.required_attribute_coverage
592            )));
593        }
594
595        if !(0.0..=100.0).contains(&self.optional_attribute_coverage) {
596            return Err(CleanroomError::validation_error(format!(
597                "Optional attribute coverage must be between 0.0 and 100.0, got {}",
598                self.optional_attribute_coverage
599            )));
600        }
601
602        Ok(())
603    }
604}
605
606impl CollectorConfig {
607    /// Validate collector configuration
608    pub fn validate(&self) -> Result<()> {
609        // If use_existing is true, endpoint must be provided
610        if self.use_existing && self.endpoint.is_none() {
611            return Err(CleanroomError::validation_error(
612                "Collector endpoint must be provided when use_existing = true",
613            ));
614        }
615
616        // Validate endpoint format if provided
617        if let Some(ref endpoint) = self.endpoint {
618            if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
619                return Err(CleanroomError::validation_error(format!(
620                    "Collector endpoint must start with http:// or https://, got: {}",
621                    endpoint
622                )));
623            }
624        }
625
626        // Validate timeouts
627        if self.health_check_timeout == 0 {
628            return Err(CleanroomError::validation_error(
629                "Health check timeout must be greater than 0 seconds",
630            ));
631        }
632
633        Ok(())
634    }
635}