ggen-domain 5.1.3

Domain logic layer for ggen - pure business logic without CLI dependencies
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
//! Auto-Promotion Pipeline - Autonomic Package Promotion/Retirement (v4.0.0 feature)
//!
//! Continuously monitors marketplace packages and autonomously:
//! - Promotes packages meeting SLO, guard, and adoption criteria
//! - Deprecates packages declining in performance or adoption
//! - Suggests ontology improvements based on package health
//! - Ensures all promotions are justified by Γ signals and doctrine-aligned
//!
//! **Note**: This module requires the `marketplace-v2` feature and is scheduled for v4.0.0

#![cfg(feature = "marketplace-v2")]

use super::ahi_contract::AHIError;
use super::marketplace_scorer::{MarketplaceScorer, PackageRecommendation, PackageScore};
use super::ontology_proposal_engine::OntologySigmaProposal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// A promotion or deprecation decision with justification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromotionDecision {
    pub decision_id: String,
    pub package_name: String,
    pub package_version: String,
    pub decision_type: DecisionType,
    pub justified_by: Vec<String>, // Observation IDs from Γ
    pub metrics_snapshot: DecisionMetrics,
    pub previous_status: String,
    pub new_status: String,
    pub confidence: f64,                  // 0-1, how confident in this decision
    pub reversible: bool,                 // Can this decision be reverted?
    pub revert_condition: Option<String>, // Condition to trigger revert
    pub timestamp: u64,
}

/// Type of promotion/deprecation decision
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DecisionType {
    /// Promote to Featured/Active status
    Promote,
    /// Deprecate from active status
    Deprecate,
    /// Quarantine due to critical issues
    Quarantine,
    /// Restore from deprecation
    Restore,
}

impl std::fmt::Display for DecisionType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DecisionType::Promote => write!(f, "Promote"),
            DecisionType::Deprecate => write!(f, "Deprecate"),
            DecisionType::Quarantine => write!(f, "Quarantine"),
            DecisionType::Restore => write!(f, "Restore"),
        }
    }
}

/// Metrics snapshot at decision time
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionMetrics {
    pub slo_score: f64,
    pub guard_score: f64,
    pub adoption_score: f64,
    pub risk_score: f64,
    pub composite_score: f64,
    pub uptime_percent: f64,
    pub error_rate: f64,
    pub active_tenants: usize,
    pub growth_trend: f64,
}

/// Pipeline state and history
#[derive(Debug, Clone)]
pub struct AutoPromotionPipeline {
    #[allow(dead_code)]
    scorer: MarketplaceScorer,
    promotion_threshold: f64,   // Composite score ≥ this to promote
    deprecation_threshold: f64, // Composite score ≤ this to deprecate
    quarantine_threshold: f64,  // Risk score ≥ this to quarantine
    decisions: Vec<PromotionDecision>,
    decision_history: HashMap<String, Vec<PromotionDecision>>,
    ontology_suggestions: Vec<OntologySigmaProposal>,
}

impl AutoPromotionPipeline {
    /// Create new pipeline with default thresholds
    pub fn new() -> Self {
        Self {
            scorer: MarketplaceScorer::new(),
            promotion_threshold: 80.0,   // 80+ = promote
            deprecation_threshold: 40.0, // 40- = deprecate
            quarantine_threshold: 80.0,  // Risk 80+ = quarantine
            decisions: Vec::new(),
            decision_history: HashMap::new(),
            ontology_suggestions: Vec::new(),
        }
    }

    /// Set custom thresholds
    pub fn with_thresholds(promotion: f64, deprecation: f64, quarantine: f64) -> Self {
        let mut pipeline = Self::new();
        pipeline.promotion_threshold = promotion;
        pipeline.deprecation_threshold = deprecation;
        pipeline.quarantine_threshold = quarantine;
        pipeline
    }

    /// Evaluate package and generate decision
    pub fn evaluate_package(
        &mut self, package_name: &str, package_version: &str, score: &PackageScore,
        observation_ids: Vec<String>,
    ) -> Result<Option<PromotionDecision>, AHIError> {
        let current_recommendation = &score.recommendation;

        // Determine decision based on scores and current status
        let decision_type = if score.risk_score >= self.quarantine_threshold {
            Some(DecisionType::Quarantine)
        } else if score.composite_score >= self.promotion_threshold {
            // Promote if not already promoted
            match current_recommendation {
                PackageRecommendation::Promoted => None, // Already promoted
                _ => Some(DecisionType::Promote),
            }
        } else if score.composite_score <= self.deprecation_threshold {
            // Deprecate if still active
            match current_recommendation {
                PackageRecommendation::Active => Some(DecisionType::Deprecate),
                PackageRecommendation::Promoted => Some(DecisionType::Deprecate),
                _ => None,
            }
        } else {
            None // No decision needed
        };

        match decision_type {
            Some(dt) => {
                let decision = self.create_decision(
                    package_name,
                    package_version,
                    dt,
                    current_recommendation.to_string(),
                    score,
                    observation_ids,
                );

                // Store decision
                self.decisions.push(decision.clone());
                self.decision_history
                    .entry(format!("{}-{}", package_name, package_version))
                    .or_insert_with(Vec::new)
                    .push(decision.clone());

                Ok(Some(decision))
            }
            None => Ok(None),
        }
    }

    /// Create a decision with full justification
    fn create_decision(
        &self, package_name: &str, package_version: &str, decision_type: DecisionType,
        previous_status: String, score: &PackageScore, observation_ids: Vec<String>,
    ) -> PromotionDecision {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let new_status = match decision_type {
            DecisionType::Promote => "Featured".to_string(),
            DecisionType::Deprecate => "Deprecated".to_string(),
            DecisionType::Quarantine => "Quarantined".to_string(),
            DecisionType::Restore => "Active".to_string(),
        };

        PromotionDecision {
            decision_id: format!("decision-{}-{}-{}", package_name, package_version, now),
            package_name: package_name.to_string(),
            package_version: package_version.to_string(),
            decision_type,
            justified_by: observation_ids,
            metrics_snapshot: DecisionMetrics {
                slo_score: score.slo_score,
                guard_score: score.guard_score,
                adoption_score: score.adoption_score,
                risk_score: score.risk_score,
                composite_score: score.composite_score,
                uptime_percent: 0.0, // Would be extracted from SLOMetrics
                error_rate: 0.0,     // Would be extracted from SLOMetrics
                active_tenants: 0,   // Would be extracted from AdoptionMetrics
                growth_trend: 0.0,   // Would be calculated from adoption history
            },
            previous_status,
            new_status,
            confidence: self.calculate_confidence(score, decision_type),
            reversible: decision_type != DecisionType::Quarantine,
            revert_condition: match decision_type {
                DecisionType::Promote => Some("Composite score drops below 60".to_string()),
                DecisionType::Deprecate => Some("Composite score recovers above 60".to_string()),
                DecisionType::Quarantine => None,
                DecisionType::Restore => Some("Risk score rises above 75 again".to_string()),
            },
            timestamp: now,
        }
    }

    /// Calculate confidence level for decision
    fn calculate_confidence(&self, score: &PackageScore, _decision_type: DecisionType) -> f64 {
        // Confidence based on how far we are from thresholds
        let distance_from_threshold = (score.composite_score
            - (if score.composite_score >= self.promotion_threshold {
                self.promotion_threshold
            } else {
                self.deprecation_threshold
            }))
        .abs();

        // Normalize distance to 0-1 confidence (further = higher confidence)
        let confidence = (distance_from_threshold / 50.0).min(1.0);

        // Boost confidence if risk is low
        let risk_adjustment = (1.0 - (score.risk_score / 100.0)) * 0.1;
        (confidence + risk_adjustment).min(1.0)
    }

    /// Generate ontology suggestions based on package performance patterns
    pub fn suggest_ontology_improvements(
        &mut self, package_scores: &[PackageScore],
    ) -> Vec<OntologySigmaProposal> {
        let mut suggestions = Vec::new();

        // Detect patterns in package performance
        let mut guard_failure_count = 0;
        let mut performance_issues_count = 0;

        for score in package_scores {
            if score.guard_score < 50.0 {
                // Guard compliance is low - count it
                guard_failure_count += 1;
            }

            if score.slo_score < 50.0 {
                performance_issues_count += 1;
            }
        }

        // Suggest guard refinements if multiple packages have guard compliance issues
        if guard_failure_count >= 3 {
            let suggestion = OntologySigmaProposal {
                id: "suggestion-guard-adaptive".to_string(),
                change_kind: super::ontology_proposal_engine::SigmaChangeKind::GuardAdjustment,
                element_name: "adaptive_guard_thresholds".to_string(),
                element_type: "Guard".to_string(),
                current_definition: Some("Static guard thresholds".to_string()),
                proposed_definition: format!(
                    "Adaptive guard thresholds based on {} packages with low compliance",
                    guard_failure_count
                ),
                justification_evidence: vec![format!(
                    "{} packages failing guard compliance",
                    guard_failure_count
                )],
                estimated_coverage_improvement: 15.0,
                estimated_performance_delta: 5.0,
                risk_score: 30.0,
                affected_patterns: vec![],
                affected_guards: vec!["all_guards".to_string()],
                doctrine_aligned: true,
            };
            suggestions.push(suggestion);
        }

        // Suggest performance patterns if widespread issues
        if performance_issues_count >= (package_scores.len() / 3) {
            let suggestion = OntologySigmaProposal {
                id: "suggestion-perf-pattern".to_string(),
                change_kind: super::ontology_proposal_engine::SigmaChangeKind::NewPattern,
                element_name: "performance_optimization_pattern".to_string(),
                element_type: "Pattern".to_string(),
                current_definition: None,
                proposed_definition: "Pattern to address widespread performance issues".to_string(),
                justification_evidence: vec![format!(
                    "{} packages with low SLO scores",
                    performance_issues_count
                )],
                estimated_coverage_improvement: 20.0,
                estimated_performance_delta: -100.0,
                risk_score: 40.0,
                affected_patterns: vec!["all_packages".to_string()],
                affected_guards: vec![],
                doctrine_aligned: true,
            };
            suggestions.push(suggestion);
        }

        self.ontology_suggestions = suggestions.clone();
        suggestions
    }

    /// Get all promotion decisions
    pub fn decisions(&self) -> &[PromotionDecision] {
        &self.decisions
    }

    /// Get decisions for specific package
    pub fn package_decision_history(
        &self, package_name: &str, package_version: &str,
    ) -> Option<&Vec<PromotionDecision>> {
        self.decision_history
            .get(&format!("{}-{}", package_name, package_version))
    }

    /// Get latest decision for package
    pub fn latest_decision_for_package(
        &self, package_name: &str, package_version: &str,
    ) -> Option<&PromotionDecision> {
        self.decision_history
            .get(&format!("{}-{}", package_name, package_version))
            .and_then(|decisions| decisions.last())
    }

    /// Check if a decision can be reverted
    pub fn can_revert(&self, decision: &PromotionDecision) -> bool {
        decision.reversible
    }

    /// Generate audit report
    pub fn audit_report(&self) -> Vec<String> {
        let mut report = Vec::new();

        report.push(format!(
            "Auto-Promotion Pipeline Report - {} decisions",
            self.decisions.len()
        ));
        report.push(format!("Promotion threshold: {}", self.promotion_threshold));
        report.push(format!(
            "Deprecation threshold: {}",
            self.deprecation_threshold
        ));
        report.push(format!(
            "Quarantine threshold: {}",
            self.quarantine_threshold
        ));

        let promote_count = self
            .decisions
            .iter()
            .filter(|d| d.decision_type == DecisionType::Promote)
            .count();
        let deprecate_count = self
            .decisions
            .iter()
            .filter(|d| d.decision_type == DecisionType::Deprecate)
            .count();
        let quarantine_count = self
            .decisions
            .iter()
            .filter(|d| d.decision_type == DecisionType::Quarantine)
            .count();

        report.push(format!(
            "Decisions: {} promotions, {} deprecations, {} quarantines",
            promote_count, deprecate_count, quarantine_count
        ));
        report.push(format!(
            "Ontology suggestions: {}",
            self.ontology_suggestions.len()
        ));

        report
    }
}

impl Default for AutoPromotionPipeline {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_pipeline_creation() {
        let pipeline = AutoPromotionPipeline::new();
        assert_eq!(pipeline.promotion_threshold, 80.0);
        assert_eq!(pipeline.deprecation_threshold, 40.0);
        assert_eq!(pipeline.quarantine_threshold, 80.0);
    }

    #[test]
    fn test_custom_thresholds() {
        let pipeline = AutoPromotionPipeline::with_thresholds(85.0, 35.0, 75.0);
        assert_eq!(pipeline.promotion_threshold, 85.0);
        assert_eq!(pipeline.deprecation_threshold, 35.0);
        assert_eq!(pipeline.quarantine_threshold, 75.0);
    }

    #[test]
    fn test_confidence_calculation() {
        let pipeline = AutoPromotionPipeline::new();

        let score = PackageScore {
            package_id: PackageId::new("pkg".to_string(), "1.0".to_string()),
            slo_score: 85.0,
            guard_score: 80.0,
            economic_score: 75.0,
            adoption_score: 90.0,
            risk_score: 20.0,
            composite_score: 82.0,
            recommendation: PackageRecommendation::Active,
            scored_at: 1000,
        };

        let confidence = pipeline.calculate_confidence(&score, DecisionType::Promote);
        assert!(confidence > 0.0 && confidence <= 1.0);
    }

    #[test]
    fn test_audit_report() {
        let pipeline = AutoPromotionPipeline::new();
        let report = pipeline.audit_report();
        assert!(report.len() > 0);
        assert!(report.join(" ").contains("Pipeline Report"));
    }
}