#![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;
#[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>, pub metrics_snapshot: DecisionMetrics,
pub previous_status: String,
pub new_status: String,
pub confidence: f64, pub reversible: bool, pub revert_condition: Option<String>, pub timestamp: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DecisionType {
Promote,
Deprecate,
Quarantine,
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"),
}
}
}
#[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,
}
#[derive(Debug, Clone)]
pub struct AutoPromotionPipeline {
#[allow(dead_code)]
scorer: MarketplaceScorer,
promotion_threshold: f64, deprecation_threshold: f64, quarantine_threshold: f64, decisions: Vec<PromotionDecision>,
decision_history: HashMap<String, Vec<PromotionDecision>>,
ontology_suggestions: Vec<OntologySigmaProposal>,
}
impl AutoPromotionPipeline {
pub fn new() -> Self {
Self {
scorer: MarketplaceScorer::new(),
promotion_threshold: 80.0, deprecation_threshold: 40.0, quarantine_threshold: 80.0, decisions: Vec::new(),
decision_history: HashMap::new(),
ontology_suggestions: Vec::new(),
}
}
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
}
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;
let decision_type = if score.risk_score >= self.quarantine_threshold {
Some(DecisionType::Quarantine)
} else if score.composite_score >= self.promotion_threshold {
match current_recommendation {
PackageRecommendation::Promoted => None, _ => Some(DecisionType::Promote),
}
} else if score.composite_score <= self.deprecation_threshold {
match current_recommendation {
PackageRecommendation::Active => Some(DecisionType::Deprecate),
PackageRecommendation::Promoted => Some(DecisionType::Deprecate),
_ => None,
}
} else {
None };
match decision_type {
Some(dt) => {
let decision = self.create_decision(
package_name,
package_version,
dt,
current_recommendation.to_string(),
score,
observation_ids,
);
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),
}
}
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, error_rate: 0.0, active_tenants: 0, growth_trend: 0.0, },
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,
}
}
fn calculate_confidence(&self, score: &PackageScore, _decision_type: DecisionType) -> f64 {
let distance_from_threshold = (score.composite_score
- (if score.composite_score >= self.promotion_threshold {
self.promotion_threshold
} else {
self.deprecation_threshold
}))
.abs();
let confidence = (distance_from_threshold / 50.0).min(1.0);
let risk_adjustment = (1.0 - (score.risk_score / 100.0)) * 0.1;
(confidence + risk_adjustment).min(1.0)
}
pub fn suggest_ontology_improvements(
&mut self, package_scores: &[PackageScore],
) -> Vec<OntologySigmaProposal> {
let mut suggestions = Vec::new();
let mut guard_failure_count = 0;
let mut performance_issues_count = 0;
for score in package_scores {
if score.guard_score < 50.0 {
guard_failure_count += 1;
}
if score.slo_score < 50.0 {
performance_issues_count += 1;
}
}
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);
}
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
}
pub fn decisions(&self) -> &[PromotionDecision] {
&self.decisions
}
pub fn package_decision_history(
&self, package_name: &str, package_version: &str,
) -> Option<&Vec<PromotionDecision>> {
self.decision_history
.get(&format!("{}-{}", package_name, package_version))
}
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())
}
pub fn can_revert(&self, decision: &PromotionDecision) -> bool {
decision.reversible
}
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"));
}
}