use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MetricFieldType {
Usize,
F32,
Bool,
OptionF32,
OptionUsize,
HashMapStringUsize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum MetricCategory {
Content,
Structure,
Media,
Seo,
Links,
Technical,
Accessibility,
Mobile,
Authority,
Language,
Forms,
StructuredData,
Branding,
UserExperience,
Business,
Internationalization,
Performance,
Security,
Analytics,
ErrorHandling,
}
impl MetricCategory {
pub fn all() -> Vec<MetricCategory> {
vec![
MetricCategory::Content,
MetricCategory::Structure,
MetricCategory::Media,
MetricCategory::Seo,
MetricCategory::Links,
MetricCategory::Technical,
MetricCategory::Accessibility,
MetricCategory::Mobile,
MetricCategory::Authority,
MetricCategory::Language,
MetricCategory::Forms,
MetricCategory::StructuredData,
MetricCategory::Branding,
MetricCategory::UserExperience,
MetricCategory::Business,
MetricCategory::Internationalization,
MetricCategory::Performance,
MetricCategory::Security,
MetricCategory::Analytics,
MetricCategory::ErrorHandling,
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ScoringFunction {
Linear {
min_value: f32,
max_value: f32,
reverse_scoring: bool, },
Logarithmic {
base: f32,
scaling_factor: f32,
},
StepFunction {
steps: Vec<(f32, f32)>, },
CustomFunction {
function_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileMetricConfig {
pub weight: f32,
pub enabled: bool,
pub thresholds: ThresholdSet,
pub penalty_multiplier: f32,
pub bonus_conditions: Vec<BonusCondition>,
pub scoring_function_override: Option<ScoringFunction>,
}
impl Default for ProfileMetricConfig {
fn default() -> Self {
Self {
weight: 1.0,
enabled: true,
thresholds: ThresholdSet::default(),
penalty_multiplier: 1.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThresholdSet {
pub excellent: f32, pub good: f32, pub fair: f32, pub poor: f32, }
impl Default for ThresholdSet {
fn default() -> Self {
Self {
excellent: 100.0,
good: 80.0,
fair: 60.0,
poor: 30.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BonusCondition {
pub condition_type: BonusConditionType,
pub bonus_points: f32,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BonusConditionType {
ValueBetween { min: f32, max: f32 },
ValueAbove { threshold: f32 },
ValueBelow { threshold: f32 },
ValueEquals { target: f32 },
}
#[derive(Debug, Clone)]
pub struct ValidationRule {
pub rule_type: ValidationRuleType,
pub error_message: &'static str,
}
#[derive(Debug, Clone)]
pub enum ValidationRuleType {
MinValue(f32),
MaxValue(f32),
Range(f32, f32),
NotNaN,
NotInfinite,
}
#[derive(Debug, Clone)]
pub enum MetricValue {
Usize(usize),
F32(f32),
Bool(bool),
OptionF32(Option<f32>),
OptionUsize(Option<usize>),
HashMapStringUsize(HashMap<String, usize>),
}
impl MetricValue {
pub fn as_f32(&self) -> f32 {
match self {
MetricValue::Usize(v) => *v as f32,
MetricValue::F32(v) => *v,
MetricValue::Bool(v) => {
if *v {
1.0
} else {
0.0
}
}
MetricValue::OptionF32(Some(v)) => *v,
MetricValue::OptionF32(None) => 0.0,
MetricValue::OptionUsize(Some(v)) => *v as f32,
MetricValue::OptionUsize(None) => 0.0,
MetricValue::HashMapStringUsize(map) => map.len() as f32,
}
}
pub fn is_some(&self) -> bool {
match self {
MetricValue::OptionF32(opt) => opt.is_some(),
MetricValue::OptionUsize(opt) => opt.is_some(),
_ => true,
}
}
}
#[derive(Debug, Clone)]
pub struct MetricDefinition {
pub field_name: &'static str,
pub field_type: MetricFieldType,
pub category: MetricCategory,
pub scoring_function: ScoringFunction,
pub profile_configurations: HashMap<String, ProfileMetricConfig>,
pub validation_rules: Vec<ValidationRule>,
pub description: &'static str,
pub html_only_available: bool,
pub requires_network: bool,
pub default_weight: f32,
pub default_enabled: bool,
}
pub struct MetricRegistry {
metrics: HashMap<String, MetricDefinition>,
categories: HashMap<MetricCategory, Vec<String>>,
}
impl MetricRegistry {
pub fn new() -> Self {
let mut registry = Self {
metrics: HashMap::new(),
categories: HashMap::new(),
};
registry.register_all_metrics();
registry
}
pub fn get_metric_definition(&self, field_name: &str) -> Option<&MetricDefinition> {
self.metrics.get(field_name)
}
pub fn get_profile_config(
&self,
field_name: &str,
profile: &str,
) -> Option<&ProfileMetricConfig> {
self.metrics
.get(field_name)?
.profile_configurations
.get(profile)
}
pub fn list_metrics_by_category(&self, category: MetricCategory) -> Vec<&MetricDefinition> {
self.categories
.get(&category)
.map(|metric_names| {
metric_names
.iter()
.filter_map(|name| self.metrics.get(name))
.collect()
})
.unwrap_or_default()
}
pub fn all_metrics(&self) -> Vec<&MetricDefinition> {
self.metrics.values().collect()
}
fn register_metric(&mut self, metric: MetricDefinition) {
let field_name = metric.field_name.to_string();
let category = metric.category.clone();
self.categories
.entry(category)
.or_insert_with(Vec::new)
.push(field_name.clone());
self.metrics.insert(field_name, metric);
}
fn register_all_metrics(&mut self) {
for metric in create_all_metrics() {
self.register_metric(metric);
}
}
}
pub static METRIC_REGISTRY: Lazy<MetricRegistry> = Lazy::new(|| MetricRegistry::new());
fn create_enhanced_metrics() -> Vec<MetricDefinition> {
let mut metrics = Vec::new();
metrics.push(MetricDefinition {
field_name: "word_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Content,
scoring_function: ScoringFunction::CustomFunction {
function_name: "word_count".to_string(),
},
profile_configurations: create_word_count_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Word count cannot be negative",
}],
description: "Total number of words in the main content",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
});
metrics.push(MetricDefinition {
field_name: "readability_fk",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::CustomFunction {
function_name: "readability_scorer".to_string(),
},
profile_configurations: create_readability_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::Range(0.0, 30.0),
error_message: "Flesch-Kincaid score should be between 0 and 30",
}],
description: "Flesch-Kincaid readability score (lower = more readable)",
html_only_available: true,
requires_network: false,
default_weight: 0.20,
default_enabled: true,
});
metrics.push(MetricDefinition {
field_name: "main_text_ratio",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0,
reverse_scoring: false,
},
profile_configurations: create_text_ratio_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::Range(0.0, 1.0),
error_message: "Text ratio must be between 0 and 1",
}],
description: "Ratio of main content text to total page text",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
});
metrics.push(MetricDefinition {
field_name: "headings_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Structure,
scoring_function: ScoringFunction::StepFunction {
steps: vec![
(0.0, 0.0), (1.0, 40.0), (3.0, 70.0), (5.0, 90.0), (10.0, 100.0), ],
},
profile_configurations: create_headings_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Heading count cannot be negative",
}],
description: "Total number of headings (h1-h6) in the content",
html_only_available: true,
requires_network: false,
default_weight: 0.18,
default_enabled: true,
});
metrics.push(MetricDefinition {
field_name: "title_len",
field_type: MetricFieldType::Usize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::CustomFunction {
function_name: "title_length_scorer".to_string(),
},
profile_configurations: create_title_length_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Title length cannot be negative",
}],
description: "Length of the page title in characters",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
});
metrics.push(MetricDefinition {
field_name: "images_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 20.0,
reverse_scoring: false,
},
profile_configurations: create_images_count_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Image count cannot be negative",
}],
description: "Total number of images on the page",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
});
metrics
}
fn create_word_count_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"content_article".to_string(),
ProfileMetricConfig {
weight: 0.25,
enabled: true,
thresholds: ThresholdSet {
excellent: 1500.0,
good: 800.0,
fair: 400.0,
poor: 100.0,
},
penalty_multiplier: 2.5, bonus_conditions: vec![BonusCondition {
condition_type: BonusConditionType::ValueBetween {
min: 1500.0,
max: 3000.0,
},
bonus_points: 5.0,
description: "Optimal content length for articles".to_string(),
}],
scoring_function_override: None,
},
);
configs.insert(
"news".to_string(),
ProfileMetricConfig {
weight: 0.15,
enabled: true,
thresholds: ThresholdSet {
excellent: 800.0,
good: 400.0,
fair: 200.0,
poor: 50.0,
},
penalty_multiplier: 1.2,
bonus_conditions: vec![BonusCondition {
condition_type: BonusConditionType::ValueBetween {
min: 300.0,
max: 800.0,
},
bonus_points: 3.0,
description: "Optimal length for news articles".to_string(),
}],
scoring_function_override: None,
},
);
configs.insert(
"product".to_string(),
ProfileMetricConfig {
weight: 0.08,
enabled: true,
thresholds: ThresholdSet {
excellent: 300.0,
good: 150.0,
fair: 75.0,
poor: 25.0,
},
penalty_multiplier: 0.8, bonus_conditions: vec![BonusCondition {
condition_type: BonusConditionType::ValueBetween {
min: 100.0,
max: 500.0,
},
bonus_points: 2.0,
description: "Concise product descriptions".to_string(),
}],
scoring_function_override: None,
},
);
configs.insert(
"portfolio".to_string(),
ProfileMetricConfig {
weight: 0.05,
enabled: true,
thresholds: ThresholdSet {
excellent: 200.0,
good: 100.0,
fair: 50.0,
poor: 10.0,
},
penalty_multiplier: 0.5,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs
}
fn create_readability_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"content_article".to_string(),
ProfileMetricConfig {
weight: 0.20,
enabled: true,
thresholds: ThresholdSet {
excellent: 10.0, good: 14.0,
fair: 18.0,
poor: 22.0,
},
penalty_multiplier: 1.8,
bonus_conditions: vec![BonusCondition {
condition_type: BonusConditionType::ValueBetween {
min: 6.0,
max: 12.0,
},
bonus_points: 5.0,
description: "Optimal readability range".to_string(),
}],
scoring_function_override: None,
},
);
configs.insert(
"news".to_string(),
ProfileMetricConfig {
weight: 0.15,
enabled: true,
thresholds: ThresholdSet {
excellent: 12.0,
good: 16.0,
fair: 20.0,
poor: 24.0,
},
penalty_multiplier: 1.3,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs.insert(
"product".to_string(),
ProfileMetricConfig {
weight: 0.08,
enabled: true,
thresholds: ThresholdSet {
excellent: 15.0,
good: 20.0,
fair: 25.0,
poor: 30.0,
},
penalty_multiplier: 0.8,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs.insert(
"portfolio".to_string(),
ProfileMetricConfig {
weight: 0.05,
enabled: false, thresholds: ThresholdSet::default(),
penalty_multiplier: 0.5,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs
}
fn create_ratio_metric_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"Content Article".to_string(), ProfileMetricConfig {
weight: 0.10,
enabled: true,
thresholds: ThresholdSet {
excellent: 0.7, good: 0.5, fair: 0.3, poor: 0.1, },
penalty_multiplier: 1.5,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs.insert(
"Blog".to_string(), ProfileMetricConfig {
weight: 0.08,
enabled: true,
thresholds: ThresholdSet {
excellent: 0.65,
good: 0.45,
fair: 0.25,
poor: 0.10,
},
penalty_multiplier: 1.2,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs.insert(
"General".to_string(), ProfileMetricConfig {
weight: 0.08,
enabled: true,
thresholds: ThresholdSet {
excellent: 0.6,
good: 0.4,
fair: 0.2,
poor: 0.1,
},
penalty_multiplier: 1.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs
}
fn create_text_ratio_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"content_article".to_string(),
ProfileMetricConfig {
weight: 0.15,
enabled: true,
thresholds: ThresholdSet {
excellent: 0.7,
good: 0.5,
fair: 0.3,
poor: 0.1,
},
penalty_multiplier: 2.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs.insert(
"product".to_string(),
ProfileMetricConfig {
weight: 0.08,
enabled: true,
thresholds: ThresholdSet {
excellent: 0.4,
good: 0.2,
fair: 0.1,
poor: 0.05,
},
penalty_multiplier: 1.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs
}
fn create_headings_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"content_article".to_string(),
ProfileMetricConfig {
weight: 0.18,
enabled: true,
thresholds: ThresholdSet {
excellent: 8.0,
good: 5.0,
fair: 3.0,
poor: 1.0,
},
penalty_multiplier: 2.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs
}
fn create_title_length_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"content_article".to_string(),
ProfileMetricConfig {
weight: 0.20,
enabled: true,
thresholds: ThresholdSet {
excellent: 60.0, good: 70.0,
fair: 80.0,
poor: 100.0,
},
penalty_multiplier: 1.5,
bonus_conditions: vec![BonusCondition {
condition_type: BonusConditionType::ValueBetween {
min: 30.0,
max: 60.0,
},
bonus_points: 3.0,
description: "Optimal title length for SEO".to_string(),
}],
scoring_function_override: None,
},
);
configs
}
fn create_images_count_profile_configs() -> HashMap<String, ProfileMetricConfig> {
let mut configs = HashMap::new();
configs.insert(
"product".to_string(),
ProfileMetricConfig {
weight: 0.30,
enabled: true,
thresholds: ThresholdSet {
excellent: 8.0,
good: 4.0,
fair: 2.0,
poor: 0.0,
},
penalty_multiplier: 2.0,
bonus_conditions: vec![BonusCondition {
condition_type: BonusConditionType::ValueAbove { threshold: 5.0 },
bonus_points: 5.0,
description: "Rich visual content for products".to_string(),
}],
scoring_function_override: None,
},
);
configs.insert(
"content_article".to_string(),
ProfileMetricConfig {
weight: 0.10,
enabled: true,
thresholds: ThresholdSet {
excellent: 5.0,
good: 2.0,
fair: 1.0,
poor: 0.0,
},
penalty_multiplier: 1.0,
bonus_conditions: Vec::new(),
scoring_function_override: None,
},
);
configs
}
fn create_all_metrics() -> Vec<MetricDefinition> {
vec![
MetricDefinition {
field_name: "word_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Content,
scoring_function: ScoringFunction::CustomFunction {
function_name: "word_count".to_string(),
},
profile_configurations: create_word_count_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "word_count cannot be negative",
}],
description: "Total number of words in the main content",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "main_text_ratio",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: create_text_ratio_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "main_text_ratio cannot be negative",
}],
description: "Ratio of main content text to total page text",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "avg_sentence_len",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "avg_sentence_len cannot be negative",
}],
description: "Average sentence length in words",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "readability_fk",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: create_readability_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "readability_fk cannot be negative",
}],
description: "Flesch-Kincaid readability score",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "stopword_ratio",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "stopword_ratio cannot be negative",
}],
description: "Ratio of stop words to total words",
html_only_available: true,
requires_network: false,
default_weight: 0.05,
default_enabled: true,
},
MetricDefinition {
field_name: "unique_word_ratio",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: create_ratio_metric_profile_configs(), validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "unique_word_ratio cannot be negative",
}],
description: "Ratio of unique words to total words (lexical diversity)",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "sentence_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "sentence_count cannot be negative",
}],
description: "Total number of sentences in the content",
html_only_available: true,
requires_network: false,
default_weight: 0.08,
default_enabled: true,
},
MetricDefinition {
field_name: "reading_time_minutes",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "reading_time_minutes cannot be negative",
}],
description: "Estimated reading time in minutes",
html_only_available: true,
requires_network: false,
default_weight: 0.12,
default_enabled: true,
},
MetricDefinition {
field_name: "content_density",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: create_ratio_metric_profile_configs(), validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_density cannot be negative",
}],
description: "Density of meaningful content vs markup",
html_only_available: true,
requires_network: false,
default_weight: 0.18,
default_enabled: true,
},
MetricDefinition {
field_name: "content_to_html_ratio",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: create_ratio_metric_profile_configs(), validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_to_html_ratio cannot be negative",
}],
description: "Ratio of content text to total HTML size",
html_only_available: true,
requires_network: false,
default_weight: 0.12,
default_enabled: true,
},
MetricDefinition {
field_name: "content_structure_penalty",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_structure_penalty cannot be negative",
}],
description: "Penalty for poor content structure organization",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "heading_depth",
field_type: MetricFieldType::Usize,
category: MetricCategory::Structure,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "heading_depth cannot be negative",
}],
description: "Maximum depth of heading hierarchy (h1-h6)",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "heading_order_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Structure,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "heading_order_score cannot be negative",
}],
description: "Score for logical heading order (h1, h2, h3, etc.)",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "headings_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Structure,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: create_headings_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "headings_count cannot be negative",
}],
description: "Total number of heading elements (h1-h6)",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "paragraph_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Structure,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "paragraph_count cannot be negative",
}],
description: "Number of paragraph elements",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "heading_structure_valid",
field_type: MetricFieldType::Bool,
category: MetricCategory::Structure,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "heading_structure_valid cannot be negative",
}],
description: "Whether heading structure follows accessibility guidelines",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "images_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: create_images_count_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "images_count cannot be negative",
}],
description: "Total number of image elements",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "image_alt_coverage",
field_type: MetricFieldType::F32,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "image_alt_coverage cannot be negative",
}],
description: "Percentage of images with alt text",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "missing_alt_text_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "missing_alt_text_count cannot be negative",
}],
description: "Number of images missing alt text",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "video_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "video_count cannot be negative",
}],
description: "Number of video elements",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "audio_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "audio_count cannot be negative",
}],
description: "Number of audio elements",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "avg_video_duration_seconds",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 300.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Video duration cannot be negative",
}],
description: "Average duration of video content in seconds",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: false,
},
MetricDefinition {
field_name: "avg_audio_duration_seconds",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 300.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Audio duration cannot be negative",
}],
description: "Average duration of audio content in seconds",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: false,
},
MetricDefinition {
field_name: "images_to_text_ratio",
field_type: MetricFieldType::F32,
category: MetricCategory::Media,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0, reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "images_to_text_ratio cannot be negative",
}],
description: "Ratio of images to text content",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "title_len",
field_type: MetricFieldType::Usize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: create_title_length_profile_configs(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "title_len cannot be negative",
}],
description: "Length of the page title in characters",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "meta_desc_len",
field_type: MetricFieldType::OptionUsize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "meta_desc_len cannot be negative",
}],
description: "Length of meta description in characters",
html_only_available: true,
requires_network: false,
default_weight: 0.18,
default_enabled: true,
},
MetricDefinition {
field_name: "has_canonical",
field_type: MetricFieldType::Bool,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "has_canonical cannot be negative",
}],
description: "Whether page has canonical URL specified",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "og_tags",
field_type: MetricFieldType::Usize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "og_tags cannot be negative",
}],
description: "Number of Open Graph meta tags",
html_only_available: true,
requires_network: false,
default_weight: 0.12,
default_enabled: true,
},
MetricDefinition {
field_name: "twitter_tags",
field_type: MetricFieldType::Usize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "twitter_tags cannot be negative",
}],
description: "Number of Twitter Card meta tags",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "robots_noindex",
field_type: MetricFieldType::Bool,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "robots_noindex cannot be negative",
}],
description: "Whether robots meta tag prevents indexing",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "seo_penalty",
field_type: MetricFieldType::F32,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "seo_penalty cannot be negative",
}],
description: "SEO penalty score for common issues",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "viewport_tag_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "viewport_tag_present cannot be negative",
}],
description: "Whether viewport meta tag is present",
html_only_available: true,
requires_network: false,
default_weight: 0.08,
default_enabled: true,
},
MetricDefinition {
field_name: "favicon_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "favicon_present cannot be negative",
}],
description: "Whether favicon is specified",
html_only_available: true,
requires_network: false,
default_weight: 0.05,
default_enabled: true,
},
MetricDefinition {
field_name: "total_links",
field_type: MetricFieldType::Usize,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "total_links cannot be negative",
}],
description: "Total number of anchor links",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "internal_links",
field_type: MetricFieldType::Usize,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "internal_links cannot be negative",
}],
description: "Number of internal links",
html_only_available: true,
requires_network: false,
default_weight: 0.12,
default_enabled: true,
},
MetricDefinition {
field_name: "external_links",
field_type: MetricFieldType::Usize,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "external_links cannot be negative",
}],
description: "Number of external links",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "broken_link_rate",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "broken_link_rate cannot be negative",
}],
description: "Percentage of broken links",
html_only_available: false,
requires_network: true,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "anchor_text_diversity",
field_type: MetricFieldType::F32,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "anchor_text_diversity cannot be negative",
}],
description: "Diversity of anchor text used in links",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "nofollow_links",
field_type: MetricFieldType::Usize,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "nofollow_links cannot be negative",
}],
description: "Number of nofollow links",
html_only_available: true,
requires_network: false,
default_weight: 0.08,
default_enabled: true,
},
MetricDefinition {
field_name: "rel_attribute_counts",
field_type: MetricFieldType::HashMapStringUsize,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 10.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Rel attribute counts cannot be negative",
}],
description: "Count of different rel attribute values",
html_only_available: true,
requires_network: false,
default_weight: 0.05,
default_enabled: false,
},
MetricDefinition {
field_name: "links_to_content_ratio",
field_type: MetricFieldType::F32,
category: MetricCategory::Links,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "links_to_content_ratio cannot be negative",
}],
description: "Ratio of links to content size",
html_only_available: true,
requires_network: false,
default_weight: 0.12,
default_enabled: true,
},
MetricDefinition {
field_name: "html_bytes",
field_type: MetricFieldType::Usize,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "html_bytes cannot be negative",
}],
description: "Size of HTML content in bytes",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "script_bytes",
field_type: MetricFieldType::Usize,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "script_bytes cannot be negative",
}],
description: "Size of external script content in bytes",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "style_bytes",
field_type: MetricFieldType::Usize,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "style_bytes cannot be negative",
}],
description: "Size of external stylesheet content in bytes",
html_only_available: true,
requires_network: false,
default_weight: 0.12,
default_enabled: true,
},
MetricDefinition {
field_name: "inline_script_bytes",
field_type: MetricFieldType::Usize,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "inline_script_bytes cannot be negative",
}],
description: "Size of inline script content in bytes",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "inline_style_bytes",
field_type: MetricFieldType::Usize,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "inline_style_bytes cannot be negative",
}],
description: "Size of inline style content in bytes",
html_only_available: true,
requires_network: false,
default_weight: 0.08,
default_enabled: true,
},
MetricDefinition {
field_name: "color_contrast_violations",
field_type: MetricFieldType::Usize,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "color_contrast_violations cannot be negative",
}],
description: "Number of WCAG color contrast violations found",
html_only_available: true,
requires_network: false,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "aria_labels_missing",
field_type: MetricFieldType::Usize,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "aria_labels_missing cannot be negative",
}],
description: "Number of elements missing required ARIA labels",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "keyboard_navigation_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "keyboard_navigation_score cannot be negative",
}],
description: "Score for keyboard navigation support",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "wcag_aa_compliance_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "wcag_aa_compliance_score cannot be negative",
}],
description: "WCAG AA compliance score",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "wcag_aaa_compliance_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "wcag_aaa_compliance_score cannot be negative",
}],
description: "WCAG AAA compliance score",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "screen_reader_compatibility",
field_type: MetricFieldType::F32,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "screen_reader_compatibility cannot be negative",
}],
description: "Screen reader compatibility score",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "accessibility_landmarks_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Accessibility,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "accessibility_landmarks_count cannot be negative",
}],
description: "Number of accessibility landmark elements",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "mobile_friendly_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Mobile,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "mobile_friendly_score cannot be negative",
}],
description: "Mobile-friendliness score",
html_only_available: true,
requires_network: false,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "viewport_configured",
field_type: MetricFieldType::Bool,
category: MetricCategory::Mobile,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "viewport_configured cannot be negative",
}],
description: "Whether viewport is properly configured for mobile",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "touch_target_violations",
field_type: MetricFieldType::Usize,
category: MetricCategory::Mobile,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "touch_target_violations cannot be negative",
}],
description: "Number of touch targets that are too small",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "mobile_usability_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Mobile,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "mobile_usability_score cannot be negative",
}],
description: "Overall mobile usability score",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "has_author",
field_type: MetricFieldType::Bool,
category: MetricCategory::Authority,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "has_author cannot be negative",
}],
description: "Whether page has author information",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "has_publish_date",
field_type: MetricFieldType::Bool,
category: MetricCategory::Authority,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "has_publish_date cannot be negative",
}],
description: "Whether page has publication date",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "references_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Authority,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "references_count cannot be negative",
}],
description: "Number of external references or citations",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "language_confidence",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Language,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 1.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::Range(0.0, 1.0),
error_message: "Language confidence must be between 0 and 1",
}],
description: "Confidence score for detected language",
html_only_available: true,
requires_network: false,
default_weight: 0.05,
default_enabled: false,
},
MetricDefinition {
field_name: "forms_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Forms,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "forms_count cannot be negative",
}],
description: "Number of form elements on the page",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "form_fields_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Forms,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "form_fields_count cannot be negative",
}],
description: "Total number of form input fields",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "required_fields_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Forms,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 10.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "Required fields count cannot be negative",
}],
description: "Number of required form fields",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "form_validation_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Forms,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "form_validation_present cannot be negative",
}],
description: "Whether form validation is implemented",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "form_accessibility_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Forms,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "form_accessibility_score cannot be negative",
}],
description: "Accessibility score for form elements",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "captcha_implementation",
field_type: MetricFieldType::Bool,
category: MetricCategory::Forms,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "captcha_implementation cannot be negative",
}],
description: "Whether CAPTCHA is implemented on forms",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "json_ld_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::StructuredData,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "json_ld_count cannot be negative",
}],
description: "Number of JSON-LD structured data blocks",
html_only_available: true,
requires_network: false,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "microdata_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::StructuredData,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "microdata_count cannot be negative",
}],
description: "Number of microdata structured data elements",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "rdfa_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::StructuredData,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "rdfa_count cannot be negative",
}],
description: "Number of RDFa structured data elements",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "structured_data_score",
field_type: MetricFieldType::F32,
category: MetricCategory::StructuredData,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "structured_data_score cannot be negative",
}],
description: "Overall structured data implementation score",
html_only_available: true,
requires_network: false,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "brand_color_consistency",
field_type: MetricFieldType::F32,
category: MetricCategory::Branding,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "brand_color_consistency cannot be negative",
}],
description: "Consistency of brand colors throughout the page",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "font_consistency_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Branding,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "font_consistency_score cannot be negative",
}],
description: "Consistency of font usage throughout the page",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "logo_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Branding,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "logo_present cannot be negative",
}],
description: "Whether a logo is present on the page",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "design_system_compliance",
field_type: MetricFieldType::F32,
category: MetricCategory::Branding,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "design_system_compliance cannot be negative",
}],
description: "Compliance with design system guidelines",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "interactive_elements_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::UserExperience,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "interactive_elements_count cannot be negative",
}],
description: "Number of interactive elements (buttons, inputs, etc.)",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "social_sharing_buttons",
field_type: MetricFieldType::Usize,
category: MetricCategory::UserExperience,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "social_sharing_buttons cannot be negative",
}],
description: "Number of social sharing buttons",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "search_functionality_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::UserExperience,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "search_functionality_present cannot be negative",
}],
description: "Whether search functionality is available",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "contact_info_accessible",
field_type: MetricFieldType::Bool,
category: MetricCategory::UserExperience,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "contact_info_accessible cannot be negative",
}],
description: "Whether contact information is easily accessible",
html_only_available: true,
requires_network: false,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "conversion_elements_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::UserExperience,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "conversion_elements_count cannot be negative",
}],
description: "Number of conversion-oriented elements (CTAs, etc.)",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "contact_methods_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Business,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "contact_methods_count cannot be negative",
}],
description: "Number of contact methods available (email, phone, etc.)",
html_only_available: true,
requires_network: false,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "business_hours_displayed",
field_type: MetricFieldType::Bool,
category: MetricCategory::Business,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "business_hours_displayed cannot be negative",
}],
description: "Whether business hours are displayed",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "location_information_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Business,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "location_information_present cannot be negative",
}],
description: "Whether location information is present",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "hreflang_tags_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Internationalization,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "hreflang_tags_count cannot be negative",
}],
description: "Number of hreflang tags for international SEO",
html_only_available: true,
requires_network: false,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "language_variants_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Internationalization,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "language_variants_count cannot be negative",
}],
description: "Number of language variants detected",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "largest_contentful_paint",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "largest_contentful_paint cannot be negative",
}],
description: "Largest Contentful Paint performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.25,
default_enabled: true,
},
MetricDefinition {
field_name: "interaction_to_next_paint",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "interaction_to_next_paint cannot be negative",
}],
description: "Interaction to Next Paint performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "cumulative_layout_shift",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "cumulative_layout_shift cannot be negative",
}],
description: "Cumulative Layout Shift performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "time_to_first_byte",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "time_to_first_byte cannot be negative",
}],
description: "Time to First Byte server response time",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "dom_content_loaded_time",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "dom_content_loaded_time cannot be negative",
}],
description: "DOM Content Loaded event timing",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "first_contentful_paint",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "first_contentful_paint cannot be negative",
}],
description: "First Contentful Paint performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "time_to_interactive",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "time_to_interactive cannot be negative",
}],
description: "Time to Interactive performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "total_blocking_time",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "total_blocking_time cannot be negative",
}],
description: "Total Blocking Time performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "speed_index",
field_type: MetricFieldType::OptionF32,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "speed_index cannot be negative",
}],
description: "Speed Index performance metric",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "total_page_size_kb",
field_type: MetricFieldType::Usize,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "total_page_size_kb cannot be negative",
}],
description: "Total page size in kilobytes",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "number_of_http_requests",
field_type: MetricFieldType::Usize,
category: MetricCategory::Performance,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "number_of_http_requests cannot be negative",
}],
description: "Total number of HTTP requests made",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "https_enabled",
field_type: MetricFieldType::Bool,
category: MetricCategory::Security,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "https_enabled cannot be negative",
}],
description: "Whether HTTPS is enabled",
html_only_available: false,
requires_network: true,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "csp_header_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Security,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "csp_header_present cannot be negative",
}],
description: "Whether Content Security Policy header is present",
html_only_available: false,
requires_network: true,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "hsts_enabled",
field_type: MetricFieldType::Bool,
category: MetricCategory::Security,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "hsts_enabled cannot be negative",
}],
description: "Whether HTTP Strict Transport Security is enabled",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "xss_protection_enabled",
field_type: MetricFieldType::Bool,
category: MetricCategory::Security,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "xss_protection_enabled cannot be negative",
}],
description: "Whether XSS protection headers are enabled",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "content_type_nosniff",
field_type: MetricFieldType::Bool,
category: MetricCategory::Security,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_type_nosniff cannot be negative",
}],
description: "Whether X-Content-Type-Options nosniff is set",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "referrer_policy_set",
field_type: MetricFieldType::Bool,
category: MetricCategory::Security,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "referrer_policy_set cannot be negative",
}],
description: "Whether Referrer-Policy header is set",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "google_analytics_present",
field_type: MetricFieldType::Bool,
category: MetricCategory::Analytics,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "google_analytics_present cannot be negative",
}],
description: "Whether Google Analytics is detected",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "conversion_tracking_setup",
field_type: MetricFieldType::Bool,
category: MetricCategory::Analytics,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "conversion_tracking_setup cannot be negative",
}],
description: "Whether conversion tracking is set up",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "tag_manager_implemented",
field_type: MetricFieldType::Bool,
category: MetricCategory::Analytics,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "tag_manager_implemented cannot be negative",
}],
description: "Whether tag management is implemented",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "robots_txt_referenced",
field_type: MetricFieldType::Bool,
category: MetricCategory::ErrorHandling,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "robots_txt_referenced cannot be negative",
}],
description: "Whether robots.txt is properly referenced",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "error_pages_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::ErrorHandling,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "error_pages_count cannot be negative",
}],
description: "Number of error pages encountered",
html_only_available: false,
requires_network: true,
default_weight: 0.15,
default_enabled: true,
},
MetricDefinition {
field_name: "redirects_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::ErrorHandling,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "redirects_count cannot be negative",
}],
description: "Number of redirects in the request chain",
html_only_available: false,
requires_network: true,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "charset_declared",
field_type: MetricFieldType::Bool,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "charset_declared cannot be negative",
}],
description: "Whether the document declares a character encoding",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "content_organization_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_organization_score cannot be negative",
}],
description: "Score indicating how well content is organized and structured",
html_only_available: true,
requires_network: false,
default_weight: 0.3,
default_enabled: true,
},
MetricDefinition {
field_name: "content_section_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_section_count cannot be negative",
}],
description: "Number of distinct content sections identified",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "content_chunk_diversity",
field_type: MetricFieldType::F32,
category: MetricCategory::Content,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "content_chunk_diversity cannot be negative",
}],
description: "Measure of diversity in content chunk types and structure",
html_only_available: true,
requires_network: false,
default_weight: 0.2,
default_enabled: true,
},
MetricDefinition {
field_name: "meta_keywords_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "meta_keywords_count cannot be negative",
}],
description: "Number of keywords in meta keywords tag (deprecated but tracked)",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "feed_urls_count",
field_type: MetricFieldType::Usize,
category: MetricCategory::Seo,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "feed_urls_count cannot be negative",
}],
description: "Number of RSS/Atom feed URLs found",
html_only_available: true,
requires_network: false,
default_weight: 0.1,
default_enabled: true,
},
MetricDefinition {
field_name: "technical_score",
field_type: MetricFieldType::F32,
category: MetricCategory::Technical,
scoring_function: ScoringFunction::Linear {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
},
profile_configurations: HashMap::new(),
validation_rules: vec![ValidationRule {
rule_type: ValidationRuleType::MinValue(0.0),
error_message: "technical_score cannot be negative",
}],
description: "Aggregated technical quality score across all technical metrics",
html_only_available: false,
requires_network: true,
default_weight: 1.0,
default_enabled: true,
},
]
}
pub fn get_all_metrics() -> &'static Vec<MetricDefinition> {
static ALL_METRICS: Lazy<Vec<MetricDefinition>> = Lazy::new(|| create_all_metrics());
&ALL_METRICS
}
impl MetricDefinition {
pub fn toggle_field_name(&self) -> String {
format!("consider_{}", self.field_name)
}
pub fn weight_field_name(&self) -> String {
self.field_name.to_string()
}
pub fn threshold_field_name(&self) -> String {
self.field_name.to_string()
}
}
pub fn get_configurable_metrics() -> Vec<&'static MetricDefinition> {
get_all_metrics()
.iter()
.filter(|m| m.default_enabled)
.collect()
}
pub fn get_metrics_by_category(category: MetricCategory) -> Vec<&'static MetricDefinition> {
get_all_metrics()
.iter()
.filter(|m| m.category == category)
.collect()
}
pub fn get_html_only_metrics() -> Vec<&'static MetricDefinition> {
get_all_metrics()
.iter()
.filter(|m| m.html_only_available)
.collect()
}
pub fn get_network_metrics() -> Vec<&'static MetricDefinition> {
get_all_metrics()
.iter()
.filter(|m| m.requires_network)
.collect()
}
pub fn validate_metric_definitions() -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let mut field_names = std::collections::HashSet::new();
let all_metrics = get_all_metrics();
for metric in all_metrics {
if !field_names.insert(metric.field_name) {
errors.push(format!("Duplicate field name: {}", metric.field_name));
}
}
let _html_count = get_html_only_metrics().len();
let _network_count = get_network_metrics().len();
let total_count = all_metrics.len();
if total_count != 115 {
errors.push(format!("Expected 115 total metrics, found {}", total_count));
}
for metric in all_metrics {
if metric.default_enabled && metric.default_weight <= 0.0 {
errors.push(format!(
"Enabled metric '{}' has invalid weight: {}",
metric.field_name, metric.default_weight
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub trait MetricScorer: Send + Sync {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32;
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32;
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String>;
}
pub struct LinearScorer {
pub min_value: f32,
pub max_value: f32,
pub reverse_scoring: bool,
}
impl MetricScorer for LinearScorer {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
let numeric_value = value.as_f32();
let thresholds = &config.thresholds;
let base_score = if self.reverse_scoring {
match numeric_value {
v if v <= thresholds.excellent => 100.0,
v if v <= thresholds.good => {
let range = thresholds.good - thresholds.excellent;
let position = (v - thresholds.excellent) / range;
100.0 - (position * 20.0)
}
v if v <= thresholds.fair => {
let range = thresholds.fair - thresholds.good;
let position = (v - thresholds.good) / range;
80.0 - (position * 20.0)
}
v if v <= thresholds.poor => {
let range = thresholds.poor - thresholds.fair;
let position = (v - thresholds.fair) / range;
60.0 - (position * 30.0)
}
_ => 0.0,
}
} else {
match numeric_value {
v if v >= thresholds.excellent => 100.0,
v if v >= thresholds.good => {
let range = thresholds.excellent - thresholds.good;
let position = (v - thresholds.good) / range;
80.0 + (position * 20.0)
}
v if v >= thresholds.fair => {
let range = thresholds.good - thresholds.fair;
let position = (v - thresholds.fair) / range;
60.0 + (position * 20.0)
}
v if v >= thresholds.poor => {
let range = thresholds.fair - thresholds.poor;
let position = (v - thresholds.poor) / range;
30.0 + (position * 30.0)
}
v => (v / thresholds.poor) * 30.0,
}
};
base_score.clamp(0.0, 100.0)
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let mut final_score = base_score;
let numeric_value = value.as_f32();
for condition in conditions {
let bonus_applies = match &condition.condition_type {
BonusConditionType::ValueBetween { min, max } => {
numeric_value >= *min && numeric_value <= *max
}
BonusConditionType::ValueAbove { threshold } => numeric_value > *threshold,
BonusConditionType::ValueBelow { threshold } => numeric_value < *threshold,
BonusConditionType::ValueEquals { target } => (numeric_value - target).abs() < 0.01,
};
if bonus_applies {
final_score += condition.bonus_points;
}
}
final_score.clamp(0.0, 110.0) }
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
let numeric_value = value.as_f32();
for rule in rules {
match &rule.rule_type {
ValidationRuleType::MinValue(min) => {
if numeric_value < *min {
return Err(rule.error_message.to_string());
}
}
ValidationRuleType::MaxValue(max) => {
if numeric_value > *max {
return Err(rule.error_message.to_string());
}
}
ValidationRuleType::Range(min, max) => {
if numeric_value < *min || numeric_value > *max {
return Err(rule.error_message.to_string());
}
}
ValidationRuleType::NotNaN => {
if numeric_value.is_nan() {
return Err(rule.error_message.to_string());
}
}
ValidationRuleType::NotInfinite => {
if numeric_value.is_infinite() {
return Err(rule.error_message.to_string());
}
}
}
}
Ok(())
}
}
pub struct StepFunctionScorer {
pub steps: Vec<(f32, f32)>, }
impl MetricScorer for StepFunctionScorer {
fn score_metric(&self, value: MetricValue, _config: &ProfileMetricConfig) -> f32 {
let numeric_value = value.as_f32();
for (threshold, score) in &self.steps {
if numeric_value <= *threshold {
return *score;
}
}
self.steps.last().map(|(_, score)| *score).unwrap_or(0.0)
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
};
linear_scorer.apply_bonuses(base_score, value, conditions)
}
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
};
linear_scorer.validate_metric_value(value, rules)
}
}
pub struct LogarithmicScorer {
pub base: f32,
pub scaling_factor: f32,
}
impl MetricScorer for LogarithmicScorer {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
let numeric_value = value.as_f32();
if numeric_value <= 0.0 {
return 0.0;
}
let log_score = (numeric_value.log(self.base) * self.scaling_factor).clamp(0.0, 100.0);
if log_score < 50.0 {
log_score * config.penalty_multiplier
} else {
log_score
}
.clamp(0.0, 100.0)
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
};
linear_scorer.apply_bonuses(base_score, value, conditions)
}
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
};
linear_scorer.validate_metric_value(value, rules)
}
}
pub struct TitleLengthScorer;
impl MetricScorer for TitleLengthScorer {
fn score_metric(&self, value: MetricValue, config: &ProfileMetricConfig) -> f32 {
let length = value.as_f32();
let score = match length {
0.0 => 0.0, l if l < 10.0 => l * 3.0, l if l >= 30.0 && l <= 60.0 => 100.0, l if l > 60.0 && l <= 80.0 => 90.0 - ((l - 60.0) * 2.0), l if l > 80.0 && l <= 100.0 => 50.0 - ((l - 80.0) * 1.5), _ => 20.0, };
if score < 50.0 {
(score * config.penalty_multiplier).clamp(0.0, 100.0)
} else {
score
}
}
fn apply_bonuses(
&self,
base_score: f32,
value: MetricValue,
conditions: &[BonusCondition],
) -> f32 {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
};
linear_scorer.apply_bonuses(base_score, value, conditions)
}
fn validate_metric_value(
&self,
value: &MetricValue,
rules: &[ValidationRule],
) -> Result<(), String> {
let linear_scorer = LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
};
linear_scorer.validate_metric_value(value, rules)
}
}
pub fn create_metric_scorer(scoring_function: &ScoringFunction) -> Box<dyn MetricScorer> {
match scoring_function {
ScoringFunction::Linear {
min_value,
max_value,
reverse_scoring,
} => Box::new(LinearScorer {
min_value: *min_value,
max_value: *max_value,
reverse_scoring: *reverse_scoring,
}),
ScoringFunction::StepFunction { steps } => Box::new(StepFunctionScorer {
steps: steps.clone(),
}),
ScoringFunction::CustomFunction { function_name } => match function_name.as_str() {
"readability_scorer" => create_readability_scorer(),
"title_length_scorer" => create_title_length_scorer(),
_ => Box::new(LinearScorer {
min_value: 0.0,
max_value: 100.0,
reverse_scoring: false,
}),
},
ScoringFunction::Logarithmic {
base,
scaling_factor,
} => Box::new(LogarithmicScorer {
base: *base,
scaling_factor: *scaling_factor,
}),
}
}
fn create_readability_scorer() -> Box<dyn MetricScorer> {
Box::new(LinearScorer {
min_value: 0.0,
max_value: 30.0,
reverse_scoring: true, })
}
fn create_title_length_scorer() -> Box<dyn MetricScorer> {
Box::new(TitleLengthScorer)
}