use super::{
dimensions::{Dimension, Subcategory},
issue_class::IssueClass,
score::{Scaling, ScoreImpact},
severity::Severity,
};
pub struct Rule {
pub id: &'static str,
pub dimension: Dimension,
pub subcategory: Subcategory,
pub issue_class: IssueClass,
pub severity: Severity,
pub external_ref: Option<&'static str>,
pub external_level: Option<&'static str>,
pub axe_id: Option<&'static str>,
pub title: &'static str,
pub description: &'static str,
pub user_impact: &'static str,
pub technical_impact: &'static str,
pub score_impact: ScoreImpact,
pub report_visibility: ReportVisibility,
}
pub struct ReportVisibility {
pub executive: bool,
pub standard: bool,
pub technical: bool,
}
const VIS_ALL: ReportVisibility = ReportVisibility {
executive: true,
standard: true,
technical: true,
};
const VIS_STANDARD: ReportVisibility = ReportVisibility {
executive: false,
standard: true,
technical: true,
};
const VIS_TECHNICAL: ReportVisibility = ReportVisibility {
executive: false,
standard: false,
technical: true,
};
pub struct RuleLookup;
impl RuleLookup {
pub fn by_id(id: &str) -> Option<&'static Rule> {
RULES.iter().find(|r| r.id == id)
}
pub fn by_wcag(criterion: &str) -> Option<&'static Rule> {
let search = format!("WCAG {}", criterion);
RULES
.iter()
.find(|r| r.external_ref == Some(search.as_str()))
}
pub fn by_legacy_wcag_id(wcag_id: &str) -> Option<&'static Rule> {
LEGACY_WCAG_MAP
.iter()
.find(|(wid, _)| *wid == wcag_id)
.and_then(|(_, rule_id)| Self::by_id(rule_id))
}
pub fn by_dimension(dim: Dimension) -> Vec<&'static Rule> {
RULES.iter().filter(|r| r.dimension == dim).collect()
}
}
static LEGACY_WCAG_MAP: &[(&str, &str)] = &[
("1.1.1", "a11y.alt_text.missing"),
("1.2.1", "a11y.media.alternative"),
("1.2.2", "a11y.captions.missing"),
("1.2.8", "a11y.media_alternative.missing"),
("1.3.1", "a11y.structure.missing"),
("1.3.4", "a11y.orientation.restricted"),
("1.3.6", "a11y.identify_purpose.missing"),
("1.3.5", "a11y.input_purpose.missing"),
("1.4.1", "a11y.color.link_indicator"),
("1.4.3", "a11y.contrast.weak"),
("1.4.4", "a11y.resize_text.weak"),
("1.4.7", "a11y.background_audio.uncontrolled"),
("1.4.8", "a11y.visual_presentation.weak"),
("1.4.10", "a11y.reflow.missing"),
("1.4.11", "a11y.non_text_contrast.weak"),
("1.4.13", "a11y.hover.content_visibility"),
("2.1.1", "a11y.keyboard.missing"),
("2.1.2", "a11y.keyboard_trap.risk"),
("2.2.1", "a11y.timing.unadjustable"),
("2.2.3", "a11y.timing.essential"),
("2.2.4", "a11y.interruptions.uncontrollable"),
("2.2.5", "a11y.re_authenticate.data_loss"),
("2.2.6", "a11y.timeouts.unwarned"),
("2.4.1", "a11y.bypass_blocks.missing"),
("2.4.2", "a11y.page_title.missing"),
("2.4.3", "a11y.focus_order.weak"),
("2.4.4", "a11y.link_purpose.weak"),
("2.4.6", "a11y.headings.missing"),
("2.4.7", "a11y.focus_visible.missing"),
("2.4.8", "a11y.location.missing"),
("2.4.9", "a11y.link_purpose_only.weak"),
("2.3.3", "a11y.motion.reduced_motion"),
("2.4.10", "a11y.section_headings.missing"),
("2.5.1", "a11y.pointer_gestures.missing_alternative"),
("2.5.2", "a11y.pointer_cancellation.invalid"),
("2.5.3", "a11y.label_in_name.invalid"),
("2.5.4", "a11y.motion_actuation.missing_alternative"),
("2.5.5", "a11y.target_size_enhanced.small"),
("3.1.1", "a11y.language.missing"),
("3.1.3", "a11y.unusual_words.missing_definition"),
("3.1.4", "a11y.abbreviations.missing"),
("3.2.1", "a11y.on_focus.risk"),
("3.2.2", "a11y.on_input.risk"),
("3.3.1", "a11y.error_id.missing_description"),
("3.3.2", "a11y.form_labels.missing"),
("3.3.5", "a11y.help.missing"),
("4.1.1", "a11y.parsing.invalid"),
("4.1.2", "a11y.name_role.missing"),
("4.1.3", "a11y.status_messages.broken"),
("aria-hidden-focus", "a11y.aria_hidden_focus.invalid"),
("aria-prohibited-attr", "a11y.aria_prohibited_attr.invalid"),
("frame-title", "a11y.frame_title.missing"),
];
pub static RULES: &[Rule] = &[
Rule {
id: "a11y.alt_text.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::ContentAlternatives,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 1.1.1"),
external_level: Some("A"),
axe_id: Some("image-alt"),
title: "Fehlende Alternativtexte bei Bildern",
description: "Bilder haben keinen beschreibenden Alternativtext.",
user_impact: "Screenreader-Nutzer erhalten keine Bildinformation.",
technical_impact: "Nicht-konforme Bildauszeichnung im Markup.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.media.alternative",
dimension: Dimension::Accessibility,
subcategory: Subcategory::ContentAlternatives,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: Some("WCAG 1.2.1"),
external_level: Some("A"),
axe_id: Some("video-caption"),
title: "Fehlende Alternativen für reine Audio- oder Videoinhalte",
description: "Reine Audio- oder Videoinhalte haben keine alternative Textdarstellung oder Tonspur.",
user_impact: "Menschen mit Seh- oder Hörbehinderung können den Inhalt der Medien nicht erfassen.",
technical_impact: "Fehlende alternative Medienbeschreibung.",
score_impact: ScoreImpact {
base_penalty: 2.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.captions.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::ContentAlternatives,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 1.2.2"),
external_level: Some("A"),
axe_id: Some("video-caption"),
title: "Fehlende Untertitel für Videos",
description: "Für aufgezeichnete Videoinhalte sind keine Untertitel vorhanden.",
user_impact: "Gehörlose oder schwerhörige Nutzer können gesprochene Inhalte nicht wahrnehmen.",
technical_impact: "Fehlendes track-Element für Untertitel.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.media_alternative.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::ContentAlternatives,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: Some("WCAG 1.2.8"),
external_level: Some("AAA"),
axe_id: Some("media-alt"),
title: "Fehlende Medienalternative (AAA)",
description: "Für aufgezeichnete Videos oder Audios ist keine vollständige Medienalternative vorhanden.",
user_impact: "Nutzer, die keine Videos oder Audios abspielen können, erhalten keine Textalternative.",
technical_impact: "Fehlendes Transkript oder Audiobeschreibung.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.structure.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::StructureSemantics,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 1.3.1"),
external_level: Some("A"),
axe_id: None,
title: "Fehlende semantische Struktur",
description: "Struktur ist visuell, aber nicht im HTML-Code hinterlegt.",
user_impact: "Screenreader können Beziehungen zwischen Inhalten nicht erkennen.",
technical_impact: "Fehlende HTML5-Semantik und ARIA-Rollen.",
score_impact: ScoreImpact {
base_penalty: 2.5,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.headings.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::StructureSemantics,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 2.4.6"),
external_level: Some("AA"),
axe_id: Some("heading-order"),
title: "Fehlende oder unzureichende Überschriftenstruktur",
description: "Keine oder unlogische Überschriftenhierarchie.",
user_impact: "Screenreader-Navigation per Überschrift nicht möglich.",
technical_impact: "Fehlende Heading-Hierarchie im DOM.",
score_impact: ScoreImpact {
base_penalty: 20.0,
max_penalty: 20.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.section_headings.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::StructureSemantics,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 2.4.10"),
external_level: Some("AAA"),
axe_id: Some("heading-order"),
title: "Fehlende Abschnittsüberschriften",
description: "Längere Inhalte sind nicht durch Abschnittsüberschriften gegliedert.",
user_impact: "Nutzer können Inhalte nicht gezielt ansteuern.",
technical_impact: "Fehlende Sub-Headings in langen Inhaltsblöcken.",
score_impact: ScoreImpact {
base_penalty: 1.0,
max_penalty: 3.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.orientation.restricted",
dimension: Dimension::Accessibility,
subcategory: Subcategory::StructureSemantics,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("WCAG 1.3.4"),
external_level: Some("AA"),
axe_id: Some("css-orientation-lock"),
title: "Eingeschränkte Bildschirmausrichtung",
description: "Die Seite erzwingt eine bestimmte Bildschirmausrichtung (Hoch- oder Querformat).",
user_impact: "Nutzer mit fest montierten Bildschirmen (z. B. am Rollstuhl) können die Seite nicht lesen.",
technical_impact: "CSS-Medienabfragen blockieren die Ausrichtung.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.keyboard.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Critical,
external_ref: Some("WCAG 2.1.1"),
external_level: Some("A"),
axe_id: Some("keyboard"),
title: "Inhalte nicht per Tastatur bedienbar",
description: "Funktionen nur mit Maus bedienbar.",
user_impact: "Tastaturnutzer sind von Funktionen ausgeschlossen.",
technical_impact: "Click-Handler ohne Tastatur-Alternative.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.keyboard_trap.risk",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::Critical,
external_ref: Some("WCAG 2.1.2"),
external_level: Some("A"),
axe_id: Some("keyboard-trap"),
title: "Tastaturfalle — Fokus kann nicht verlassen werden",
description: "Tastaturfokus bleibt an bestimmten Stellen hängen.",
user_impact: "Seite wird faktisch unbenutzbar für Tastaturnutzer.",
technical_impact: "Fehlende Fokus-Verwaltung in modalen Elementen.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.bypass_blocks.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 2.4.1"),
external_level: Some("A"),
axe_id: Some("bypass"),
title: "Fehlende Sprungnavigation",
description: "Kein Skip-Link oder Mechanismus zum Überspringen wiederkehrender Blöcke.",
user_impact: "Tastaturnutzer müssen bei jedem Seitenwechsel durch die Navigation tabben.",
technical_impact: "Fehlender Skip-Link, fehlende HTML5-Landmarks.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 8.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.page_title.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 2.4.2"),
external_level: Some("A"),
axe_id: Some("document-title"),
title: "Fehlender oder unzureichender Seitentitel",
description: "Seite hat keinen aussagekräftigen Title-Tag.",
user_impact: "Screenreader-Nutzer können die Seite nicht identifizieren.",
technical_impact: "Leeres oder generisches <title>-Tag.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.focus_order.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("WCAG 2.4.3"),
external_level: Some("A"),
axe_id: Some("focus-order-semantics"),
title: "Unlogische Fokus-Reihenfolge",
description: "Tab-Reihenfolge entspricht nicht der visuellen Reihenfolge.",
user_impact: "Verwirrende Navigation für Tastaturnutzer.",
technical_impact: "DOM-Reihenfolge weicht von visueller Reihenfolge ab.",
score_impact: ScoreImpact {
base_penalty: 2.5,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.link_purpose.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("WCAG 2.4.4"),
external_level: Some("A"),
axe_id: Some("link-name"),
title: "Unklare oder generische Linktexte",
description: "Links mit Texten wie 'hier', 'mehr', 'weiterlesen'.",
user_impact: "Screenreader-Linklisten zeigen nichtssagende Texte.",
technical_impact: "Generische Link-Texte ohne Kontext.",
score_impact: ScoreImpact {
base_penalty: 1.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.focus_visible.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 2.4.7"),
external_level: Some("AA"),
axe_id: Some("focus-visible"),
title: "Fokus-Indikator nicht sichtbar",
description: "Kein visueller Fokus-Rahmen bei Tastaturnavigation.",
user_impact: "Tastaturnutzer sehen nicht, welches Element fokussiert ist.",
technical_impact: "CSS outline:none ohne Alternative.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 8.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.label_in_name.invalid",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Invalid,
severity: Severity::High,
external_ref: Some("WCAG 2.5.3"),
external_level: Some("A"),
axe_id: Some("label-in-name"),
title: "Sichtbarer Text ≠ zugänglicher Name",
description: "Sichtbarer Text und aria-label stimmen nicht überein.",
user_impact: "Sprachsteuerungsnutzer können Element nicht ansprechen.",
technical_impact: "aria-label weicht vom sichtbaren Text ab.",
score_impact: ScoreImpact {
base_penalty: 2.5,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.timing.unadjustable",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("WCAG 2.2.1"),
external_level: Some("A"),
axe_id: Some("meta-refresh"),
title: "Nicht anpassbare Zeitbegrenzung",
description: "Zeitbegrenzungen auf der Seite können nicht deaktiviert oder verlängert werden.",
user_impact: "Nutzer benötigen mehr Zeit für die Eingabe und können durch Timeouts Daten verlieren.",
technical_impact: "Fehlendes Refresh- oder Timeout-Handling im Script/Markup.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.timing.essential",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::Medium,
external_ref: Some("WCAG 2.2.3"),
external_level: Some("AAA"),
axe_id: Some("no-timing"),
title: "Zeitbegrenzung vorhanden (AAA)",
description: "Die Seite verwendet essenzielle Zeitbegrenzungen ohne Möglichkeit der Deaktivierung.",
user_impact: "Nutzer können Aufgaben nicht in ihrem eigenen Tempo abschließen.",
technical_impact: "Aktive Zeitbegrenzungen auf der Seite.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.interruptions.uncontrollable",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::Low,
external_ref: Some("WCAG 2.2.4"),
external_level: Some("AAA"),
axe_id: Some("no-interruptions"),
title: "Nicht unterdrückbare Unterbrechungen (AAA)",
description: "Die Seite zeigt Warnungen oder Popups, die der Nutzer nicht verschieben kann.",
user_impact: "Nutzer mit kognitiven Einschränkungen oder Screenreader-Nutzer werden abgelenkt.",
technical_impact: "Unerwartete Dialoge oder assertive Live-Regionen.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.re_authenticate.data_loss",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::Low,
external_ref: Some("WCAG 2.2.5"),
external_level: Some("AAA"),
axe_id: Some("re-authenticate"),
title: "Keine Datenspeicherung bei Reauthentifizierung (AAA)",
description: "Eingegebene Daten gehen verloren, wenn eine Session abläuft und sich der Nutzer neu anmeldet.",
user_impact: "Nutzer müssen alle Formulardaten erneut eingeben.",
technical_impact: "Fehlende Session-Datensicherung.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.timeouts.unwarned",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::Medium,
external_ref: Some("WCAG 2.2.6"),
external_level: Some("AAA"),
axe_id: Some("timeouts"),
title: "Keine Warnung vor Inaktivitäts-Timeout (AAA)",
description: "Der Nutzer wird vor einem Timeout nicht über drohenden Datenverlust gewarnt.",
user_impact: "Nutzer verlieren ungespeicherte Daten ohne Vorwarnung.",
technical_impact: "Fehlendes Timeout-Warnsystem.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.location.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 2.4.8"),
external_level: Some("AAA"),
axe_id: Some("location"),
title: "Fehlende Positionsangabe (AAA)",
description: "Es gibt keine Orientierungshilfe (wie Breadcrumbs), um den aktuellen Ort in einem Webauftritt anzuzeigen.",
user_impact: "Nutzer verlieren die Orientierung auf verschachtelten Webseiten.",
technical_impact: "Fehlende Breadcrumb-Navigation oder Sitemap-Verbindung.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.link_purpose_only.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("WCAG 2.4.9"),
external_level: Some("AAA"),
axe_id: Some("link-name"),
title: "Unklare Linkbeschriftung ohne Kontext (AAA)",
description: "Der Zweck eines Links geht nicht allein aus dem Linktext hervor.",
user_impact: "Nutzer müssen den gesamten Absatz lesen, um das Ziel eines Links zu verstehen.",
technical_impact: "Generischer Linktext wie 'Mehr lesen' oder 'Hier klicken'.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.pointer_gestures.missing_alternative",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: Some("WCAG 2.5.1"),
external_level: Some("AAA"),
axe_id: Some("pointer-gestures"),
title: "Fehlende Alternative für Zeigergesten (AAA)",
description: "Funktionen erfordern komplexe Mehrfinger- oder Pfadgesten ohne einfache Klick-Alternative.",
user_impact: "Nutzer mit motorischen Einschränkungen können die Funktionen nicht bedienen.",
technical_impact: "Erforderliche Touch-Gesten ohne Klick-Fallback.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.pointer_cancellation.invalid",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Invalid,
severity: Severity::Medium,
external_ref: Some("WCAG 2.5.2"),
external_level: Some("AAA"),
axe_id: Some("pointer-cancellation"),
title: "Auslösung von Aktionen bei Zeigerberührung (AAA)",
description: "Aktionen werden sofort beim Drücken (down-event) statt beim Loslassen (up-event) ausgeführt.",
user_impact: "Nutzer können versehentlich Aktionen auslösen, ohne diese abbrechen zu können.",
technical_impact: "Event-Handler reagieren auf mousedown oder touchstart statt click oder mouseup.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.motion_actuation.missing_alternative",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: Some("WCAG 2.5.4"),
external_level: Some("AAA"),
axe_id: Some("motion-actuation"),
title: "Fehlende Alternative für Bewegungseingabe (AAA)",
description: "Aktionen, die durch Schütteln oder Bewegen des Geräts ausgelöst werden, haben keine klassische Bedienalternative.",
user_impact: "Nutzer, die das Gerät nicht bewegen können, sind von den Funktionen ausgeschlossen.",
technical_impact: "Nutzen von Device-Motion-APIs ohne UI-Bedienelemente.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.target_size_enhanced.small",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("WCAG 2.5.5"),
external_level: Some("AAA"),
axe_id: Some("target-size"),
title: "Unzureichende Klickzielgröße (AAA)",
description: "Bedienelemente haben eine Klickfläche von weniger als 44x44 CSS-Pixeln.",
user_impact: "Nutzer mit zittrigen Händen oder auf Mobilgeräten klicken leicht auf falsche Elemente.",
technical_impact: "Zu kleine Abmessungen von Links, Buttons oder Formularelementen im CSS.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.input_purpose.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::FormsInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: Some("WCAG 1.3.5"),
external_level: Some("AA"),
axe_id: Some("autocomplete-valid"),
title: "Fehlende Eingabezweck-Kennzeichnung",
description: "Formularfelder ohne autocomplete-Attribut.",
user_impact: "Kein Autofill möglich, Eingabe fehleranfälliger.",
technical_impact: "Fehlende autocomplete-Attribute.",
score_impact: ScoreImpact {
base_penalty: 1.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.form_labels.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::FormsInteraction,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 3.3.2"),
external_level: Some("A"),
axe_id: Some("label"),
title: "Fehlende Formularbeschriftungen",
description: "Formularfelder ohne sichtbare Labels.",
user_impact: "Nutzer wissen nicht, welche Eingaben erwartet werden.",
technical_impact: "Fehlende <label>-Elemente.",
score_impact: ScoreImpact {
base_penalty: 2.5,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.error_id.missing_description",
dimension: Dimension::Accessibility,
subcategory: Subcategory::FormsInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: Some("WCAG 3.3.1"),
external_level: Some("A"),
axe_id: Some("aria-invalid-without-describedby"),
title: "Unzureichende Fehlermeldung bei Eingabefehlern",
description: "Fehlerhafte Formulareingaben werden nicht präzise identifiziert oder beschrieben.",
user_impact: "Nutzer können Fehler im Formular nicht korrigieren, weil sie nicht wissen, was falsch ist.",
technical_impact: "Fehlendes aria-invalid-Attribut oder fehlerhaft verknüpfte aria-describedby-IDs.",
score_impact: ScoreImpact {
base_penalty: 2.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.help.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::FormsInteraction,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 3.3.5"),
external_level: Some("AAA"),
axe_id: Some("help"),
title: "Fehlende Ausfüllhilfe (AAA)",
description: "Für Eingabefelder gibt es keine kontextsensitive Ausfüllhilfe.",
user_impact: "Nutzer wissen nicht, welches Format oder welche Daten in ein Feld eingegeben werden müssen.",
technical_impact: "Fehlendes Formular-Hilfesystem.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.language.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::LanguageClarity,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 3.1.1"),
external_level: Some("A"),
axe_id: Some("html-has-lang"),
title: "Fehlende Sprachangabe",
description: "Kein lang-Attribut im HTML-Tag.",
user_impact: "Screenreader liest mit falscher Aussprache vor.",
technical_impact: "Fehlendes lang-Attribut.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 12.5,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.on_focus.risk",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::High,
external_ref: Some("WCAG 3.2.1"),
external_level: Some("A"),
axe_id: Some("focus-no-context-change"),
title: "Unerwartete Kontextänderung bei Fokus",
description: "Fokuserhalt löst unerwartete Kontextänderung aus.",
user_impact: "Verwirrende Seitenänderungen für Tastaturnutzer.",
technical_impact: "onfocus-Handler lösen Navigation aus.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 8.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.on_input.risk",
dimension: Dimension::Accessibility,
subcategory: Subcategory::NavigationInteraction,
issue_class: IssueClass::Risk,
severity: Severity::Medium,
external_ref: Some("WCAG 3.2.2"),
external_level: Some("A"),
axe_id: Some("input-no-context-change"),
title: "Unerwartete Kontextänderung bei Eingabe",
description: "Formularänderung löst unangekündigte Kontextänderung aus.",
user_impact: "Verwirrende Änderungen beim Ausfüllen.",
technical_impact: "Auto-submit bei onchange.",
score_impact: ScoreImpact {
base_penalty: 2.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.unusual_words.missing_definition",
dimension: Dimension::Accessibility,
subcategory: Subcategory::LanguageClarity,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 3.1.3"),
external_level: Some("AAA"),
axe_id: Some("unusual-words"),
title: "Fehlende Erklärung unüblicher Wörter (AAA)",
description: "Für ungewöhnliche Wörter, Jargon oder Redewendungen gibt es keine Definitionen.",
user_impact: "Nutzer mit kognitiven Einschränkungen oder Fremdsprachler verstehen den Text nicht.",
technical_impact: "Fehlende Glossar-Verlinkung oder Erklärung im Text.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.abbreviations.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::LanguageClarity,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 3.1.4"),
external_level: Some("AAA"),
axe_id: Some("abbreviations"),
title: "Fehlende Erklärung von Abkürzungen (AAA)",
description: "Abkürzungen werden nicht bei ihrer ersten Verwendung oder über ein Mechanismus erklärt.",
user_impact: "Nutzer verstehen den Inhalt nicht, wenn sie die Abkürzung nicht kennen.",
technical_impact: "Fehlendes abbr-Tag mit title-Attribut.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.contrast.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("WCAG 1.4.3"),
external_level: Some("AA"),
axe_id: Some("color-contrast"),
title: "Unzureichender Farbkontrast",
description: "Text hat nicht genügend Kontrast zum Hintergrund.",
user_impact: "Text bei Sehschwäche oder ungünstigen Lichtverhältnissen schwer lesbar.",
technical_impact: "Kontrastverhältnis unter 4.5:1 für normalen Text.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 12.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.resize_text.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("WCAG 1.4.4"),
external_level: Some("AA"),
axe_id: Some("meta-viewport"),
title: "Text nicht ausreichend skalierbar",
description: "Texte können nicht auf 200% vergrößert werden.",
user_impact: "Nutzer mit Sehschwäche verlieren Inhalte bei Vergrößerung.",
technical_impact: "Feste Pixelwerte, overflow:hidden.",
score_impact: ScoreImpact {
base_penalty: 2.5,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.reflow.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 1.4.10"),
external_level: Some("AA"),
axe_id: Some("css-overflow-hidden"),
title: "Kein Reflow bei 320 CSS-Pixeln",
description: "Seite erfordert horizontales Scrollen bei einer Ansichtsbreite von 320 CSS-Pixeln.",
user_impact: "Nutzer, die auf 400 % zoomen, müssen in zwei Richtungen scrollen — Inhalte werden unzugänglich.",
technical_impact: "Feste Breiten oder overflow:hidden verhindern responsives Reflow.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.non_text_contrast.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("WCAG 1.4.11"),
external_level: Some("AA"),
axe_id: Some("non-text-contrast"),
title: "Unzureichender Kontrast bei UI-Elementen",
description: "Bedienelemente haben nicht genügend Kontrast.",
user_impact: "Interaktive Elemente schwer erkennbar.",
technical_impact: "Kontrastverhältnis UI-Elemente unter 3:1.",
score_impact: ScoreImpact {
base_penalty: 1.5,
max_penalty: 5.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.color.link_indicator",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("WCAG 1.4.1"),
external_level: Some("A"),
axe_id: Some("link-in-text-block"),
title: "Link nur durch Farbe unterscheidbar",
description: "Inline-Link hebt sich vom Fließtext nur durch Farbe ab, ohne weitere visuelle Unterscheidung.",
user_impact: "Nutzer mit Farbsehschwäche können Links im Text nicht erkennen.",
technical_impact: "Kein Unterstrich, Fettschrift oder anderes nicht-farbliches Merkmal vorhanden.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.motion.reduced_motion",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 2.3.3"),
external_level: Some("AAA"),
axe_id: Some("prefers-reduced-motion"),
title: "Animationen ohne prefers-reduced-motion",
description: "Seite definiert Animationen oder Übergänge, berücksichtigt aber nicht das Systemsetting prefers-reduced-motion.",
user_impact: "Nutzer mit vestibulären Störungen oder Photosensitivität können Bewegung nicht deaktivieren.",
technical_impact: "Kein @media (prefers-reduced-motion) in den Stylesheets.",
score_impact: ScoreImpact {
base_penalty: 1.0,
max_penalty: 4.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.hover.content_visibility",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("WCAG 1.4.13"),
external_level: Some("AA"),
axe_id: Some("content-on-hover-focus"),
title: "Inzugängliche Hover- oder Fokus-Inhalte (Tooltips)",
description: "Zusatzinhalte bei Hover/Fokus (Tooltips) sind unzugänglich, nicht steuerbar oder unvollständig verknüpft.",
user_impact: "Nutzer können Tooltip-Inhalte nicht lesen, diese nicht per Tastatur schließen, oder sie werden von Screenreadern ignoriert.",
technical_impact: "Nutzung des nativen title-Attributs als einzige Beschriftung, unvollständige ARIA-Verknüpfung (aria-describedby) oder fehlende Steuerungsmöglichkeiten.",
score_impact: ScoreImpact {
base_penalty: 2.0,
max_penalty: 6.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.background_audio.uncontrolled",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Risk,
severity: Severity::Medium,
external_ref: Some("WCAG 1.4.7"),
external_level: Some("AAA"),
axe_id: Some("background-audio"),
title: "Nicht regulierbare Hintergrundgeräusche (AAA)",
description: "Hintergrundgeräusche können nicht abgeschaltet oder leiser gestellt werden.",
user_impact: "Nutzer mit Hörgeräten oder kognitiven Einschränkungen werden stark abgelenkt.",
technical_impact: "Automatisch abspielendes Audio ohne Steuerungselemente.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.visual_presentation.weak",
dimension: Dimension::Accessibility,
subcategory: Subcategory::VisualPresentation,
issue_class: IssueClass::Weak,
severity: Severity::Low,
external_ref: Some("WCAG 1.4.8"),
external_level: Some("AAA"),
axe_id: Some("visual-presentation"),
title: "Eingeschränkte visuelle Darstellung von Text (AAA)",
description: "Der Text hat unzureichenden Zeilenabstand oder ist im Blocksatz formatiert.",
user_impact: "Nutzer mit Sehbehinderungen oder Legasthenie haben Probleme beim Lesen.",
technical_impact: "Unzureichendes CSS-Styling für Zeilenabstand oder Blocksatz-Formatierung.",
score_impact: ScoreImpact {
base_penalty: 0.5,
max_penalty: 1.5,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.aria_hidden_focus.invalid",
dimension: Dimension::Accessibility,
subcategory: Subcategory::TechnicalRobustness,
issue_class: IssueClass::Invalid,
severity: Severity::High,
external_ref: Some("WCAG 4.1.2"),
external_level: Some("A"),
axe_id: Some("aria-hidden-focus"),
title: "Fokussierbares Element versteckt vor Screenreadern",
description: "Interaktives Element ist über aria-hidden=\"true\" vor AT versteckt, aber per Tastatur erreichbar.",
user_impact: "Tastaturnutzer erreichen Elemente, die Screenreader nicht ankündigen — unsichtbare Stolperfallen.",
technical_impact: "aria-hidden=\"true\" auf fokussierbarem Element oder seinem Vorfahren.",
score_impact: ScoreImpact {
base_penalty: 6.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.status_messages.broken",
dimension: Dimension::Accessibility,
subcategory: Subcategory::TechnicalRobustness,
issue_class: IssueClass::Invalid,
severity: Severity::Medium,
external_ref: Some("WCAG 4.1.3"),
external_level: Some("AA"),
axe_id: Some("aria-live-region-role"),
title: "Statusmeldungen nicht programmatisch bestimmbar",
description: "ARIA-Live-Regionen haben eine inkonsistente Kombination aus role und aria-live.",
user_impact: "Screenreader kündigt Statusmeldungen nicht korrekt an.",
technical_impact: "Konflikt zwischen role (alert/status/log/timer) und aria-live-Wert.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 8.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.identify_purpose.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::TechnicalRobustness,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: Some("WCAG 1.3.6"),
external_level: Some("AAA"),
axe_id: None,
title: "Landmark ohne unterscheidbaren Namen",
description: "Mehrere gleichartige Landmarks ohne aria-label zur Unterscheidung.",
user_impact: "Screenreader-Nutzer können identische Landmarks nicht voneinander unterscheiden.",
technical_impact: "Fehlende aria-label/aria-labelledby auf doppelten Landmarks.",
score_impact: ScoreImpact {
base_penalty: 1.0,
max_penalty: 4.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "a11y.name_role.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::TechnicalRobustness,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 4.1.2"),
external_level: Some("A"),
axe_id: None,
title: "Fehlende Name/Rolle bei Bedienelementen",
description: "Interaktive Elemente ohne zugänglichen Namen oder erkennbare Rolle.",
user_impact: "Screenreader kann Funktion nicht vermitteln.",
technical_impact: "Fehlende ARIA-Labels und -Rollen.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 12.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.aria_prohibited_attr.invalid",
dimension: Dimension::Accessibility,
subcategory: Subcategory::TechnicalRobustness,
issue_class: IssueClass::Invalid,
severity: Severity::High,
external_ref: Some("WCAG 4.1.2"),
external_level: Some("A"),
axe_id: Some("aria-prohibited-attr"),
title: "Verbotene ARIA-Attribute",
description: "ARIA-Attribute werden auf einer Rolle verwendet, die sie nicht erlaubt.",
user_impact: "Screenreader erhalten widersprüchliche oder ungültige Semantik.",
technical_impact: "ARIA-Attribute passen nicht zur impliziten oder expliziten Rolle.",
score_impact: ScoreImpact {
base_penalty: 4.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.frame_title.missing",
dimension: Dimension::Accessibility,
subcategory: Subcategory::ContentAlternatives,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: Some("WCAG 4.1.2"),
external_level: Some("A"),
axe_id: Some("frame-title"),
title: "Iframe ohne zugänglichen Namen",
description: "Iframe oder Frame hat keinen title, aria-label oder aria-labelledby.",
user_impact: "Screenreader-Nutzer erkennen Zweck und Inhalt des eingebetteten Frames nicht.",
technical_impact: "Fehlender zugänglicher Name auf iframe/frame.",
score_impact: ScoreImpact {
base_penalty: 4.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "a11y.parsing.invalid",
dimension: Dimension::Accessibility,
subcategory: Subcategory::TechnicalRobustness,
issue_class: IssueClass::Invalid,
severity: Severity::Critical,
external_ref: Some("WCAG 4.1.1"),
external_level: Some("A"),
axe_id: Some("duplicate-id"),
title: "Fehlerhaftes Markup (Parsing)",
description: "HTML-Code weist schwerwiegende Syntaxfehler auf, wie z. B. doppelte IDs.",
user_impact: "Screenreader können den AX-Tree nicht korrekt aufbauen und überspringen Elemente.",
technical_impact: "Doppelte IDs in der DOM-Struktur oder unvollständige HTML-Tags.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Logarithmic,
},
report_visibility: VIS_ALL,
},
Rule {
id: "perf.lcp.slow",
dimension: Dimension::Performance,
subcategory: Subcategory::LoadBehavior,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("CWV:LCP"),
external_level: None,
axe_id: None,
title: "Langsamer Largest Contentful Paint",
description: "Hauptinhalt wird zu spät sichtbar.",
user_impact: "Nutzer sieht lange leere oder unvollständige Seite.",
technical_impact: "LCP über 2500ms — Netzwerk, Rendering oder Ressourcen zu langsam.",
score_impact: ScoreImpact {
base_penalty: 25.0,
max_penalty: 25.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "perf.fcp.slow",
dimension: Dimension::Performance,
subcategory: Subcategory::LoadBehavior,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("CWV:FCP"),
external_level: None,
axe_id: None,
title: "Langsamer First Contentful Paint",
description: "Erster sichtbarer Inhalt erscheint zu spät.",
user_impact: "Nutzer wartet zu lange auf erste Rückmeldung.",
technical_impact: "FCP über 1800ms — Server oder Rendering-Blockade.",
score_impact: ScoreImpact {
base_penalty: 25.0,
max_penalty: 25.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "perf.cls.unstable",
dimension: Dimension::Performance,
subcategory: Subcategory::VisualStability,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: Some("CWV:CLS"),
external_level: None,
axe_id: None,
title: "Layout-Verschiebungen während des Ladens",
description: "Seitenelemente springen während des Ladens.",
user_impact: "Nutzer klickt versehentlich auf falsches Element.",
technical_impact: "CLS über 0.1 — fehlende Dimensionen, nachladende Inhalte.",
score_impact: ScoreImpact {
base_penalty: 25.0,
max_penalty: 25.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "perf.tbt.high",
dimension: Dimension::Performance,
subcategory: Subcategory::Interactivity,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: Some("CWV:TBT"),
external_level: None,
axe_id: None,
title: "Hohe Blocking-Zeit",
description: "Hauptthread blockiert zu lange durch JavaScript.",
user_impact: "Seite reagiert verzögert auf Nutzereingaben.",
technical_impact: "TBT/INP über 200ms — zu viel JavaScript-Arbeit im Hauptthread.",
score_impact: ScoreImpact {
base_penalty: 25.0,
max_penalty: 25.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "perf.ttfb.slow",
dimension: Dimension::Performance,
subcategory: Subcategory::LoadBehavior,
issue_class: IssueClass::Risk,
severity: Severity::Medium,
external_ref: Some("CWV:TTFB"),
external_level: None,
axe_id: None,
title: "Langsame Server-Antwortzeit",
description: "Server reagiert zu langsam auf die Anfrage.",
user_impact: "Gesamte Ladezeit wird verzögert.",
technical_impact: "TTFB über 800ms — Server-Performance oder Netzwerk-Latenz.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "perf.resources.heavy",
dimension: Dimension::Performance,
subcategory: Subcategory::ResourceUsage,
issue_class: IssueClass::Risk,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "Hoher Ressourcenverbrauch",
description: "Seite lädt zu viele oder zu große Ressourcen.",
user_impact: "Langsame Ladezeiten, hoher Datenverbrauch auf Mobilgeräten.",
technical_impact: "Gesamtgröße > 3MB, zu viele Requests, unkomprimiert.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "perf.dom.large",
dimension: Dimension::Performance,
subcategory: Subcategory::TechnicalComplexity,
issue_class: IssueClass::Risk,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Großes DOM",
description: "Übermäßig viele DOM-Elemente.",
user_impact: "Seite reagiert langsam auf Interaktionen.",
technical_impact: "DOM-Größe belastet Browser-Rendering und Speicher.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_TECHNICAL,
},
Rule {
id: "seo.title.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::SnippetMetadata,
issue_class: IssueClass::Missing,
severity: Severity::Critical,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlender Seitentitel",
description: "Kein <title>-Tag vorhanden.",
user_impact: "Seite wird in Suchergebnissen ohne aussagekräftigen Titel angezeigt.",
technical_impact: "Leeres oder fehlendes <title>-Element.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "seo.title.weak",
dimension: Dimension::Seo,
subcategory: Subcategory::SnippetMetadata,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "Ungünstige Titellänge",
description: "Titel zu kurz (<30 Zeichen) oder zu lang (>60 Zeichen).",
user_impact: "Snippet wird in Suchergebnissen suboptimal dargestellt.",
technical_impact: "Title-Tag außerhalb des empfohlenen Zeichenbereichs.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.description.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::SnippetMetadata,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Meta-Description",
description: "Keine Meta-Description vorhanden.",
user_impact: "Suchmaschine generiert eigene Beschreibung, oft suboptimal.",
technical_impact: "Fehlendes <meta name=\"description\">-Element.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "seo.description.weak",
dimension: Dimension::Seo,
subcategory: Subcategory::SnippetMetadata,
issue_class: IssueClass::Weak,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Ungünstige Description-Länge",
description: "Meta-Description zu kurz (<120) oder zu lang (>160 Zeichen).",
user_impact: "Snippet wird abgeschnitten oder ist zu knapp.",
technical_impact: "Description außerhalb des empfohlenen Zeichenbereichs.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.viewport.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::SnippetMetadata,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Viewport-Meta-Tag",
description: "Kein Viewport-Meta-Tag vorhanden.",
user_impact: "Seite wird auf Mobilgeräten nicht korrekt dargestellt.",
technical_impact: "Fehlendes <meta name=\"viewport\">-Element.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "seo.h1.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::ContentStructure,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende H1-Überschrift",
description: "Seite hat keine H1-Überschrift.",
user_impact: "Suchmaschine kann Hauptthema der Seite schlechter erkennen.",
technical_impact: "Kein <h1>-Element vorhanden.",
score_impact: ScoreImpact {
base_penalty: 15.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "seo.h1.multiple",
dimension: Dimension::Seo,
subcategory: Subcategory::ContentStructure,
issue_class: IssueClass::Weak,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Mehrere H1-Überschriften",
description: "Seite hat mehr als eine H1-Überschrift.",
user_impact: "Thematische Fokussierung der Seite unklar.",
technical_impact: "Mehrere <h1>-Elemente im DOM.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.headings.issue",
dimension: Dimension::Seo,
subcategory: Subcategory::ContentStructure,
issue_class: IssueClass::Weak,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Heading-Probleme",
description: "Übersprungene Ebenen, leere Headings oder zu lange Headings.",
user_impact: "Strukturelle Signale für Suchmaschine geschwächt.",
technical_impact: "Heading-Hierarchie nicht korrekt.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 9.0,
occurrence_scaling: Scaling::Linear,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.canonical.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::Indexability,
issue_class: IssueClass::Opportunity,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Canonical-URL",
description: "Kein Canonical-Link vorhanden.",
user_impact: "Mögliche Duplicate-Content-Probleme.",
technical_impact: "Fehlendes <link rel=\"canonical\">-Element.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.lang.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::SemanticSignals,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Sprachangabe",
description: "Kein lang-Attribut im HTML-Tag.",
user_impact: "Suchmaschine kann Sprache nicht automatisch zuordnen.",
technical_impact: "Fehlendes lang-Attribut im <html>-Element.",
score_impact: ScoreImpact {
base_penalty: 3.0,
max_penalty: 3.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.og.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::SemanticSignals,
issue_class: IssueClass::Opportunity,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Open Graph Tags",
description: "Keine Open Graph Meta-Tags vorhanden.",
user_impact: "Social-Media-Vorschau der Seite ist unformatiert.",
technical_impact: "Fehlende og:-Meta-Tags.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "seo.twitter.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::SemanticSignals,
issue_class: IssueClass::Opportunity,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Twitter Card Tags",
description: "Keine Twitter Card Meta-Tags vorhanden.",
user_impact: "Twitter-Vorschau der Seite ist unformatiert.",
technical_impact: "Fehlende twitter:-Meta-Tags.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_TECHNICAL,
},
Rule {
id: "seo.https.missing",
dimension: Dimension::Seo,
subcategory: Subcategory::Indexability,
issue_class: IssueClass::Missing,
severity: Severity::Critical,
external_ref: None,
external_level: None,
axe_id: None,
title: "Kein HTTPS",
description: "Seite wird nicht über HTTPS ausgeliefert.",
user_impact: "Browser zeigt Sicherheitswarnung, Ranking-Nachteil.",
technical_impact: "Fehlende TLS-Verschlüsselung.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "sec.https.missing",
dimension: Dimension::Security,
subcategory: Subcategory::Transport,
issue_class: IssueClass::Missing,
severity: Severity::Critical,
external_ref: None,
external_level: None,
axe_id: None,
title: "Kein HTTPS",
description: "Seite wird nicht über HTTPS ausgeliefert.",
user_impact: "Daten werden unverschlüsselt übertragen.",
technical_impact: "Fehlende TLS-Verschlüsselung.",
score_impact: ScoreImpact {
base_penalty: 25.0,
max_penalty: 25.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "sec.hsts.missing",
dimension: Dimension::Security,
subcategory: Subcategory::Headers,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: None,
external_level: None,
axe_id: None,
title: "HSTS Header fehlt",
description: "Strict-Transport-Security Header nicht gesetzt.",
user_impact: "Downgrade-Angriffe auf HTTP möglich.",
technical_impact: "Fehlender HSTS-Header.",
score_impact: ScoreImpact {
base_penalty: 15.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "sec.csp.missing",
dimension: Dimension::Security,
subcategory: Subcategory::Headers,
issue_class: IssueClass::Missing,
severity: Severity::High,
external_ref: None,
external_level: None,
axe_id: None,
title: "Content Security Policy fehlt",
description: "Kein Content-Security-Policy Header gesetzt.",
user_impact: "Erhöhtes Risiko für XSS-Angriffe.",
technical_impact: "Fehlender CSP-Header.",
score_impact: ScoreImpact {
base_penalty: 15.0,
max_penalty: 15.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "sec.xcto.missing",
dimension: Dimension::Security,
subcategory: Subcategory::BrowserProtection,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "X-Content-Type-Options Header fehlt",
description: "Browser kann MIME-Typ-Sniffing durchführen.",
user_impact: "Potenzielle Sicherheitslücke durch falsche MIME-Typen.",
technical_impact: "Fehlender X-Content-Type-Options: nosniff Header.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "sec.xfo.missing",
dimension: Dimension::Security,
subcategory: Subcategory::BrowserProtection,
issue_class: IssueClass::Missing,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "X-Frame-Options Header fehlt",
description: "Seite kann in fremden iframes eingebettet werden.",
user_impact: "Clickjacking-Angriffe möglich.",
technical_impact: "Fehlender X-Frame-Options Header.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "sec.referrer.missing",
dimension: Dimension::Security,
subcategory: Subcategory::BrowserProtection,
issue_class: IssueClass::Missing,
severity: Severity::Low,
external_ref: None,
external_level: None,
axe_id: None,
title: "Referrer-Policy Header fehlt",
description: "Keine Kontrolle über Referrer-Informationen.",
user_impact: "Private URLs können an Dritte weitergegeben werden.",
technical_impact: "Fehlender Referrer-Policy Header.",
score_impact: ScoreImpact {
base_penalty: 5.0,
max_penalty: 5.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_TECHNICAL,
},
Rule {
id: "mob.viewport.missing",
dimension: Dimension::Mobile,
subcategory: Subcategory::Viewport,
issue_class: IssueClass::Missing,
severity: Severity::Critical,
external_ref: None,
external_level: None,
axe_id: None,
title: "Fehlende Viewport-Konfiguration",
description: "Kein Viewport-Meta-Tag vorhanden.",
user_impact: "Seite wird auf Mobilgeräten nicht korrekt dargestellt.",
technical_impact: "Fehlendes <meta name=\"viewport\">-Element.",
score_impact: ScoreImpact {
base_penalty: 20.0,
max_penalty: 20.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "mob.viewport.improper",
dimension: Dimension::Mobile,
subcategory: Subcategory::Viewport,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "Unvollständige Viewport-Konfiguration",
description: "Viewport-Tag fehlt width=device-width oder initial-scale=1.",
user_impact: "Seite wird möglicherweise nicht optimal skaliert.",
technical_impact: "Viewport-Meta-Tag mit unvollständiger Konfiguration.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "mob.zoom.disabled",
dimension: Dimension::Mobile,
subcategory: Subcategory::Viewport,
issue_class: IssueClass::Risk,
severity: Severity::Critical,
external_ref: None,
external_level: None,
axe_id: None,
title: "Zoom deaktiviert",
description: "user-scalable=no verhindert das Zoomen.",
user_impact: "Nutzer mit Sehschwäche können Inhalte nicht vergrößern.",
technical_impact: "Viewport-Meta mit user-scalable=no.",
score_impact: ScoreImpact {
base_penalty: 20.0,
max_penalty: 20.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
Rule {
id: "mob.touch_targets.small",
dimension: Dimension::Mobile,
subcategory: Subcategory::TouchUsability,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "Zu kleine Touch-Targets",
description: "Interaktive Elemente kleiner als 44x44 Pixel.",
user_impact: "Schwer bedienbar auf Touchscreens.",
technical_impact: "Bounding Box unter 44x44px.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "mob.fonts.small",
dimension: Dimension::Mobile,
subcategory: Subcategory::Readability,
issue_class: IssueClass::Weak,
severity: Severity::Medium,
external_ref: None,
external_level: None,
axe_id: None,
title: "Zu kleine Schriftgröße",
description: "Kleinste Schriftgröße unter 12px.",
user_impact: "Text auf Mobilgeräten schwer lesbar.",
technical_impact: "Font-size unter 12px detektiert.",
score_impact: ScoreImpact {
base_penalty: 10.0,
max_penalty: 10.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_STANDARD,
},
Rule {
id: "mob.horizontal_scroll",
dimension: Dimension::Mobile,
subcategory: Subcategory::ResponsiveLayout,
issue_class: IssueClass::Weak,
severity: Severity::High,
external_ref: None,
external_level: None,
axe_id: None,
title: "Horizontales Scrollen erforderlich",
description: "Seite ist breiter als der Viewport.",
user_impact: "Nutzer muss horizontal scrollen, Inhalte abgeschnitten.",
technical_impact: "scrollWidth > innerWidth.",
score_impact: ScoreImpact {
base_penalty: 20.0,
max_penalty: 20.0,
occurrence_scaling: Scaling::Fixed,
},
report_visibility: VIS_ALL,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rule_lookup_by_id() {
let rule = RuleLookup::by_id("a11y.alt_text.missing");
assert!(rule.is_some());
let rule = rule.unwrap();
assert_eq!(rule.dimension, Dimension::Accessibility);
assert_eq!(rule.severity, Severity::High);
}
#[test]
fn test_legacy_wcag_lookup() {
let rule = RuleLookup::by_legacy_wcag_id("1.1.1");
assert!(rule.is_some());
assert_eq!(rule.unwrap().id, "a11y.alt_text.missing");
}
#[test]
fn test_all_rules_have_dimension() {
for rule in RULES {
assert!(
rule.subcategory.dimension() == rule.dimension,
"Rule {} has subcategory {:?} which belongs to {:?}, not {:?}",
rule.id,
rule.subcategory,
rule.subcategory.dimension(),
rule.dimension
);
}
}
#[test]
fn test_all_legacy_wcag_ids_resolve() {
for (wcag_id, rule_id) in LEGACY_WCAG_MAP {
assert!(
RuleLookup::by_id(rule_id).is_some(),
"Legacy WCAG ID {} maps to rule {} which doesn't exist",
wcag_id,
rule_id
);
}
}
#[test]
fn test_unique_rule_ids() {
let mut ids: Vec<&str> = RULES.iter().map(|r| r.id).collect();
ids.sort();
let len_before = ids.len();
ids.dedup();
assert_eq!(len_before, ids.len(), "Duplicate rule IDs found");
}
}