use crate::config::enhanced_models::*;
use crate::constants::*;
use crate::metrics::definitions::{
BonusCondition, LinearScorer, MetricCategory, MetricDefinition, MetricScorer, ThresholdSet,
METRIC_REGISTRY,
};
use crate::metrics::scoring_functions::WordCountScorer;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, thiserror::Error)]
pub enum CompilationError {
#[error("Unknown metric referenced in profile: {0}")]
UnknownMetric(String),
#[error("Invalid metric configuration for {metric}: {reason}")]
InvalidMetricConfig { metric: String, reason: String },
#[error("Category weight validation failed: {0}")]
CategoryWeights(String),
#[error("Penalty compilation failed: {0}")]
PenaltyCompilation(String),
#[error("Bonus compilation failed: {0}")]
BonusCompilation(String),
#[error("Profile validation failed: {0}")]
ProfileValidation(String),
}
#[derive(Debug, Clone)]
pub struct CompiledProfile {
pub profile_name: String,
pub compiled_rules: HashMap<String, CompiledMetricRule>,
pub category_weights: HashMap<MetricCategory, f32>,
pub global_penalties: Vec<CompiledPenalty>,
pub global_bonuses: Vec<CompiledBonus>,
pub quality_bands: Arc<QualityBandConfig>,
pub content_expectations: Arc<ContentExpectations>,
}
impl Default for CompiledProfile {
fn default() -> Self {
Self {
profile_name: DEFAULT_PROFILE.to_string(),
compiled_rules: HashMap::new(),
category_weights: HashMap::new(),
global_penalties: Vec::new(),
global_bonuses: Vec::new(),
quality_bands: Arc::new(QualityBandConfig::default()),
content_expectations: Arc::new(ContentExpectations::default()),
}
}
}
#[derive(Clone)]
pub struct CompiledMetricRule {
pub metric_name: String,
pub weight: f32,
pub enabled: bool,
pub scorer: Arc<dyn MetricScorer>,
pub thresholds: ThresholdSet,
pub penalty_multiplier: f32,
pub bonus_conditions: Vec<BonusCondition>,
pub category: MetricCategory,
}
impl std::fmt::Debug for CompiledMetricRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompiledMetricRule")
.field("metric_name", &self.metric_name)
.field("weight", &self.weight)
.field("enabled", &self.enabled)
.field("scorer", &"<MetricScorer>")
.field("thresholds", &self.thresholds)
.field("penalty_multiplier", &self.penalty_multiplier)
.field("bonus_conditions", &self.bonus_conditions)
.field("category", &self.category)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct CompiledPenalty {
pub penalty_id: String,
pub trigger_condition: PenaltyTrigger,
pub penalty_type: PenaltyType,
pub description: String,
pub enabled: bool,
}
#[derive(Debug, Clone)]
pub enum PenaltyTrigger {
MetricBelow {
metric: String,
threshold: f32,
},
MetricAbove {
metric: String,
threshold: f32,
},
MetricEquals {
metric: String,
value: f32,
},
MetricMissing {
metric: String,
},
ContentDeficiency {
deficiency_type: String,
severity: f32,
},
MultipleConditions {
conditions: Vec<PenaltyTrigger>,
operator: LogicalOperator,
},
}
#[derive(Debug, Clone)]
pub enum PenaltyType {
FixedPoints { points: f32 },
Multiplier { factor: f32 },
Percentage { percent: f32 },
}
#[derive(Debug, Clone)]
pub struct CompiledBonus {
pub bonus_id: String,
pub trigger_condition: BonusTrigger,
pub bonus_points: f32,
pub description: String,
pub enabled: bool,
}
#[derive(Debug, Clone)]
pub enum BonusTrigger {
MetricExcellence {
metric: String,
threshold: f32,
},
ContentQuality {
quality_type: String,
requirement: String,
},
SynergyBonus {
metrics: Vec<String>,
combined_threshold: f32,
},
}
#[derive(Debug, Clone)]
pub enum LogicalOperator {
And,
Or,
}
pub struct ProfileCompiler {
metric_registry: &'static crate::metrics::definitions::MetricRegistry,
}
impl ProfileCompiler {
pub fn new() -> Self {
Self {
metric_registry: &METRIC_REGISTRY,
}
}
pub fn compile_profile(
&self,
profile: &EnhancedScoringProfile,
) -> Result<CompiledProfile, CompilationError> {
self.validate_profile_for_compilation(profile)?;
let compiled_rules = self.compile_metric_rules(profile)?;
let category_weights = self.extract_category_weights(&profile.category_weights)?;
let global_penalties = self.compile_penalties(&profile.penalties)?;
let global_bonuses = self.compile_bonuses(&profile.bonuses)?;
Ok(CompiledProfile {
profile_name: profile.metadata.name.clone(),
compiled_rules,
category_weights,
global_penalties,
global_bonuses,
quality_bands: Arc::new(profile.quality_bands.clone()),
content_expectations: Arc::new(profile.content_expectations.clone()),
})
}
fn validate_profile_for_compilation(
&self,
profile: &EnhancedScoringProfile,
) -> Result<(), CompilationError> {
for metric_name in profile.metric_overrides.keys() {
if self
.metric_registry
.get_metric_definition(metric_name)
.is_none()
{
return Err(CompilationError::UnknownMetric(metric_name.clone()));
}
}
let weight_sum: f32 = profile.category_weights.values().sum();
const TOLERANCE: f32 = 0.001;
if (weight_sum - 1.0).abs() > TOLERANCE {
return Err(CompilationError::CategoryWeights(format!(
"Category weights must sum to 1.0 (got {:.6}). Please adjust weights. Current weights: {:?}",
weight_sum,
profile.category_weights
)));
}
Ok(())
}
fn compile_metric_rules(
&self,
profile: &EnhancedScoringProfile,
) -> Result<HashMap<String, CompiledMetricRule>, CompilationError> {
let mut compiled_rules = HashMap::new();
let profile_name = &profile.metadata.name;
for metric_def in self.metric_registry.all_metrics() {
let metric_name = metric_def.field_name;
let metric_config =
if let Some(override_config) = profile.metric_overrides.get(metric_name) {
override_config.clone()
} else {
MetricOverride {
weight: metric_def.default_weight,
enabled: metric_def.default_enabled,
thresholds: None,
penalty_multiplier: 1.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
}
};
if !metric_config.enabled {
continue;
}
let thresholds = metric_config
.thresholds
.clone()
.unwrap_or_else(|| self.get_default_thresholds_for_metric(metric_def, profile_name));
let scorer = self.create_scorer_for_metric(metric_def, &metric_config)?;
let compiled_rule = CompiledMetricRule {
metric_name: metric_name.to_string(),
weight: metric_config.weight,
enabled: metric_config.enabled,
scorer,
thresholds,
penalty_multiplier: metric_config.penalty_multiplier,
bonus_conditions: metric_config.bonus_conditions,
category: metric_def.category.clone(),
};
compiled_rules.insert(metric_name.to_string(), compiled_rule);
}
Ok(compiled_rules)
}
fn create_scorer_for_metric(
&self,
metric_def: &MetricDefinition,
config: &MetricOverride,
) -> Result<Arc<dyn MetricScorer>, CompilationError> {
if let Some(scoring_override) = &config.scoring_function_override {
return self.create_custom_scorer(scoring_override, metric_def);
}
match &metric_def.scoring_function {
crate::metrics::definitions::ScoringFunction::Linear {
min_value,
max_value,
reverse_scoring,
} => Ok(Arc::new(LinearScorer {
min_value: *min_value,
max_value: *max_value,
reverse_scoring: *reverse_scoring,
})),
crate::metrics::definitions::ScoringFunction::CustomFunction { function_name } => {
match function_name.as_str() {
"word_count" => Ok(Arc::new(WordCountScorer)),
_ => Ok(Arc::new(LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
})),
}
}
_ => {
Ok(Arc::new(LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
}))
}
}
}
fn create_custom_scorer(
&self,
scoring_override: &ScoringFunctionOverride,
_metric_def: &MetricDefinition,
) -> Result<Arc<dyn MetricScorer>, CompilationError> {
match scoring_override {
ScoringFunctionOverride::Linear {
min_value,
max_value,
reverse_scoring,
} => Ok(Arc::new(LinearScorer {
min_value: *min_value,
max_value: *max_value,
reverse_scoring: *reverse_scoring,
})),
ScoringFunctionOverride::CustomFunction { function_name } => {
match function_name.as_str() {
"word_count" => Ok(Arc::new(WordCountScorer)),
_ => Err(CompilationError::InvalidMetricConfig {
metric: UNKNOWN_PROFILE.to_string(),
reason: format!("Unknown custom function: {}", function_name),
}),
}
}
_ => {
Ok(Arc::new(LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
}))
}
}
}
fn get_default_thresholds_for_metric(&self, metric_def: &MetricDefinition, profile_name: &str) -> ThresholdSet {
let normalized_name = profile_name
.to_lowercase()
.replace(" ", "_");
if let Some(profile_config) = metric_def.profile_configurations.get(&normalized_name) {
#[cfg(debug_assertions)]
{
eprintln!("✅ Using profile-specific thresholds for '{}' in profile '{}' (normalized: '{}'): {:?}",
metric_def.field_name, profile_name, normalized_name, profile_config.thresholds);
}
return profile_config.thresholds.clone();
}
if let Some(profile_config) = metric_def.profile_configurations.get(profile_name) {
#[cfg(debug_assertions)]
{
eprintln!("✅ Using profile-specific thresholds for '{}' in profile '{}': {:?}",
metric_def.field_name, profile_name, profile_config.thresholds);
}
return profile_config.thresholds.clone();
}
#[cfg(debug_assertions)]
{
if !metric_def.profile_configurations.is_empty() {
eprintln!("⚠️ Metric '{}' has profile configs but not for '{}' (normalized: '{}'). Available profiles: {:?}",
metric_def.field_name, profile_name, normalized_name, metric_def.profile_configurations.keys().collect::<Vec<_>>());
}
}
match metric_def.field_name {
"word_count" => ThresholdSet {
excellent: 1000.0,
good: 500.0,
fair: 200.0,
poor: 50.0,
},
"readability_fk" => ThresholdSet {
excellent: 8.0,
good: 12.0,
fair: 16.0,
poor: 20.0,
},
_ => ThresholdSet::default(),
}
}
fn extract_category_weights(
&self,
weights: &HashMap<String, f32>,
) -> Result<HashMap<MetricCategory, f32>, CompilationError> {
let mut category_weights = HashMap::new();
for (category_name, weight) in weights {
let category = self.parse_category_name(category_name)?;
category_weights.insert(category, *weight);
}
for category in MetricCategory::all() {
category_weights.entry(category).or_insert(0.0);
}
Ok(category_weights)
}
fn parse_category_name(&self, category_name: &str) -> Result<MetricCategory, CompilationError> {
match category_name.to_lowercase().as_str() {
"content" => Ok(MetricCategory::Content),
"structure" => Ok(MetricCategory::Structure),
"media" => Ok(MetricCategory::Media),
"seo" => Ok(MetricCategory::Seo),
"links" => Ok(MetricCategory::Links),
"technical" => Ok(MetricCategory::Technical),
"accessibility" => Ok(MetricCategory::Accessibility),
"mobile" => Ok(MetricCategory::Mobile),
"authority" => Ok(MetricCategory::Authority),
"language" => Ok(MetricCategory::Language),
"forms" => Ok(MetricCategory::Forms),
"structureddata" => Ok(MetricCategory::StructuredData),
"branding" => Ok(MetricCategory::Branding),
"userexperience" => Ok(MetricCategory::UserExperience),
"business" => Ok(MetricCategory::Business),
"internationalization" => Ok(MetricCategory::Internationalization),
"performance" => Ok(MetricCategory::Performance),
"security" => Ok(MetricCategory::Security),
"analytics" => Ok(MetricCategory::Analytics),
"errorhandling" => Ok(MetricCategory::ErrorHandling),
_ => Err(CompilationError::CategoryWeights(format!(
"Unknown category: {}",
category_name
))),
}
}
fn compile_penalties(
&self,
penalties: &PenaltyConfig,
) -> Result<Vec<CompiledPenalty>, CompilationError> {
let mut compiled_penalties = Vec::new();
for (penalty_id, penalty) in &penalties.severe_penalties {
let compiled = self.compile_single_penalty(penalty_id, penalty)?;
compiled_penalties.push(compiled);
}
for (penalty_id, penalty) in &penalties.moderate_penalties {
let compiled = self.compile_single_penalty(penalty_id, penalty)?;
compiled_penalties.push(compiled);
}
for (penalty_id, penalty) in &penalties.light_penalties {
let compiled = self.compile_single_penalty(penalty_id, penalty)?;
compiled_penalties.push(compiled);
}
Ok(compiled_penalties)
}
fn compile_single_penalty(
&self,
penalty_id: &str,
penalty: &GlobalPenalty,
) -> Result<CompiledPenalty, CompilationError> {
let trigger_condition = self.convert_penalty_trigger(&penalty.trigger_condition)?;
let penalty_type = self.convert_penalty_type(&penalty.penalty_type)?;
Ok(CompiledPenalty {
penalty_id: penalty_id.to_string(),
trigger_condition,
penalty_type,
description: penalty.description.clone(),
enabled: penalty.enabled,
})
}
fn convert_penalty_trigger(
&self,
trigger: &crate::config::enhanced_models::PenaltyTrigger,
) -> Result<PenaltyTrigger, CompilationError> {
use crate::config::enhanced_models::PenaltyTrigger as EMPenalty;
match trigger {
EMPenalty::MetricBelow { metric, threshold } => Ok(PenaltyTrigger::MetricBelow {
metric: metric.clone(),
threshold: *threshold,
}),
EMPenalty::MetricAbove { metric, threshold } => Ok(PenaltyTrigger::MetricAbove {
metric: metric.clone(),
threshold: *threshold,
}),
EMPenalty::MetricEquals { metric, value } => Ok(PenaltyTrigger::MetricEquals {
metric: metric.clone(),
value: *value,
}),
EMPenalty::MetricMissing { metric } => Ok(PenaltyTrigger::MetricMissing {
metric: metric.clone(),
}),
EMPenalty::ContentDeficiency {
deficiency_type,
severity,
} => Ok(PenaltyTrigger::ContentDeficiency {
deficiency_type: deficiency_type.clone(),
severity: *severity,
}),
EMPenalty::MultipleConditions {
conditions,
operator,
} => {
let converted_conditions: Result<Vec<_>, _> = conditions
.iter()
.map(|c| self.convert_penalty_trigger(c))
.collect();
Ok(PenaltyTrigger::MultipleConditions {
conditions: converted_conditions?,
operator: match operator {
crate::config::enhanced_models::LogicalOperator::And => {
LogicalOperator::And
}
crate::config::enhanced_models::LogicalOperator::Or => LogicalOperator::Or,
},
})
}
EMPenalty::MetricThreshold {
metric_name,
operator,
threshold,
} => {
match operator {
crate::config::enhanced_models::ComparisonOperator::LessThan => {
Ok(PenaltyTrigger::MetricBelow {
metric: metric_name.clone(),
threshold: *threshold,
})
}
crate::config::enhanced_models::ComparisonOperator::GreaterThan => {
Ok(PenaltyTrigger::MetricAbove {
metric: metric_name.clone(),
threshold: *threshold,
})
}
crate::config::enhanced_models::ComparisonOperator::Equals => {
Ok(PenaltyTrigger::MetricEquals {
metric: metric_name.clone(),
value: *threshold,
})
}
_ => {
Ok(PenaltyTrigger::MetricBelow {
metric: metric_name.clone(),
threshold: *threshold,
})
}
}
}
}
}
fn convert_penalty_type(
&self,
penalty_type: &crate::config::enhanced_models::PenaltyType,
) -> Result<PenaltyType, CompilationError> {
use crate::config::enhanced_models::PenaltyType as EMPenaltyType;
match penalty_type {
EMPenaltyType::FixedPoints { points } => {
Ok(PenaltyType::FixedPoints { points: *points })
}
EMPenaltyType::Multiplier { factor } => Ok(PenaltyType::Multiplier { factor: *factor }),
EMPenaltyType::CategoryPenalty {
category: _,
multiplier,
} => {
Ok(PenaltyType::Percentage {
percent: *multiplier * 100.0,
})
}
}
}
fn compile_bonuses(
&self,
bonuses: &BonusConfig,
) -> Result<Vec<CompiledBonus>, CompilationError> {
let mut compiled_bonuses = Vec::new();
for (bonus_id, bonus) in &bonuses.excellence_bonuses {
let compiled = self.compile_single_bonus(bonus_id, bonus)?;
compiled_bonuses.push(compiled);
}
for (bonus_id, bonus) in &bonuses.achievement_bonuses {
let compiled = self.compile_single_bonus(bonus_id, bonus)?;
compiled_bonuses.push(compiled);
}
for (bonus_id, bonus) in &bonuses.synergy_bonuses {
let compiled = self.compile_single_bonus(bonus_id, bonus)?;
compiled_bonuses.push(compiled);
}
Ok(compiled_bonuses)
}
fn compile_single_bonus(
&self,
bonus_id: &str,
bonus: &GlobalBonus,
) -> Result<CompiledBonus, CompilationError> {
let trigger_condition = self.convert_bonus_trigger(&bonus.trigger_condition)?;
Ok(CompiledBonus {
bonus_id: bonus_id.to_string(),
trigger_condition,
bonus_points: bonus.bonus_points,
description: bonus.description.clone(),
enabled: bonus.enabled,
})
}
fn convert_bonus_trigger(
&self,
trigger: &crate::config::enhanced_models::BonusTrigger,
) -> Result<BonusTrigger, CompilationError> {
use crate::config::enhanced_models::BonusTrigger as EMBonus;
match trigger {
EMBonus::MetricExcellence { metric, threshold } => Ok(BonusTrigger::MetricExcellence {
metric: metric.clone(),
threshold: *threshold,
}),
EMBonus::ContentQuality {
quality_type,
requirement,
} => Ok(BonusTrigger::ContentQuality {
quality_type: quality_type.clone(),
requirement: requirement.clone(),
}),
EMBonus::SynergyBonus {
metrics,
combined_threshold,
} => Ok(BonusTrigger::SynergyBonus {
metrics: metrics.clone(),
combined_threshold: *combined_threshold,
}),
EMBonus::MetricThreshold {
metric_name,
operator: _,
threshold,
} => {
Ok(BonusTrigger::MetricExcellence {
metric: metric_name.clone(),
threshold: *threshold,
})
}
EMBonus::MultipleMetricsGood { metrics, threshold } => {
Ok(BonusTrigger::SynergyBonus {
metrics: metrics.clone(),
combined_threshold: *threshold,
})
}
EMBonus::CategoryExcellence {
category,
threshold,
} => {
Ok(BonusTrigger::ContentQuality {
quality_type: QUALITY_CATEGORY_EXCELLENCE.to_string(),
requirement: format!("{} >= {}", category, threshold),
})
}
}
}
}
impl Default for ProfileCompiler {
fn default() -> Self {
Self::new()
}
}