use super::super::helpers::localized_module_name;
use crate::audit::normalized::NormalizedReport;
use crate::output::report_model::{
AffectedElement, AppendixBlock, AppendixViolation, CapabilitySignal, MethodologyBlock,
};
pub(super) fn build_appendix_block_from_normalized(
locale: &str,
normalized: &NormalizedReport,
) -> AppendixBlock {
let violations: Vec<AppendixViolation> = normalized
.findings
.iter()
.map(|f| {
let affected_elements: Vec<AffectedElement> = f
.occurrences
.iter()
.map(|occ| AffectedElement {
selector: occ.selector.clone().unwrap_or_else(|| occ.node_id.clone()),
node_id: occ.node_id.clone(),
})
.collect();
let rule_name = crate::output::builder::actions::localized_finding_text(locale, f).0;
AppendixViolation {
rule: f.wcag_criterion.clone(),
rule_name,
severity: f.severity,
message: f.description.clone(),
fix_suggestion: f.occurrences.first().and_then(|o| o.fix_suggestion.clone()),
affected_elements,
}
})
.collect();
let has_violations = !violations.is_empty();
let score_methodology = if locale == "en" {
"Score calculation: each accessibility viewport starts at 100 points; taxonomy-defined, rule-specific penalties with logarithmic scaling account for repeated violations. With desktop and mobile data, the canonical accessibility score is 70% mobile plus 30% desktop. The consolidated finding list supports evidence and remediation and does not create a third score. Conflicting signals are flagged but do not alter the score retrospectively."
} else {
"Score-Berechnung: Jeder Barrierefreiheits-Viewport startet bei 100 Punkten; taxonomiebasierte, regelspezifische Abzüge mit logarithmischer Skalierung berücksichtigen wiederholte Verstöße. Bei Desktop- und Mobile-Daten ist der kanonische Barrierefreiheits-Score 70 % Mobile plus 30 % Desktop. Die zusammengeführte Befundliste dient Evidenz und Maßnahmenplanung und erzeugt keinen dritten Score. Widersprüchliche Signale werden markiert, verändern den Score aber nicht nachträglich."
};
AppendixBlock {
violations,
score_methodology: score_methodology.to_string(),
has_violations,
}
}
pub(super) fn build_methodology(
i18n: &crate::i18n::I18n,
normalized: &NormalizedReport,
) -> MethodologyBlock {
let locale = i18n.locale();
let en = locale == "en";
let active_modules = normalized
.execution
.module_runs
.iter()
.filter(|run| {
matches!(
run.status,
crate::audit::ExecutionStatus::Completed | crate::audit::ExecutionStatus::Partial
)
})
.map(|run| run.module.clone())
.collect::<Vec<_>>()
.join(", ");
let requested_modules = normalized.execution.scope.requested_modules.join(", ");
let failed_modules = normalized
.execution
.module_runs
.iter()
.filter(|run| run.status == crate::audit::ExecutionStatus::Failed)
.map(|run| run.module.as_str())
.collect::<Vec<_>>()
.join(", ");
let scope = if en {
format!(
"Automated audit of {} for accessibility per WCAG 2.1 (level {}). Requested modules: {}.",
normalized.url, normalized.wcag_level, requested_modules
)
} else {
format!(
"Automatisierte Prüfung der Seite {} auf Barrierefreiheit nach WCAG 2.1 (Level {}). Angeforderte Module: {}.",
normalized.url, normalized.wcag_level, requested_modules
)
};
let executed_rules = normalized
.rule_outcomes
.iter()
.filter(|outcome| outcome.status != crate::wcag::RuleOutcomeStatus::Skipped)
.count();
let method = if en {
format!("The audit was performed via the Chrome DevTools Protocol (CDP) and the browser's native accessibility tree. {executed_rules} automated rule executions were recorded across the configured viewports.")
} else {
format!("Die Prüfung erfolgte über das Chrome DevTools Protocol (CDP) und den nativen Accessibility Tree des Browsers. Über die konfigurierten Viewports wurden {executed_rules} automatisierte Regelausführungen erfasst.")
};
let mut limitations = if en {
"Automated tests can detect about 30–40% of all accessibility issues. Complex aspects \
such as correct tab order, meaningful alt texts, or understandable language additionally \
require manual review."
.to_string()
} else {
"Automatisierte Tests können ca. 30–40% aller Barrierefreiheitsprobleme erkennen. \
Komplexe Aspekte wie korrekte Tab-Reihenfolge, sinnvolle Alt-Texte oder \
verständliche Sprache erfordern zusätzlich manuelle Prüfung."
.to_string()
};
if normalized.execution.quality.qualified_results {
limitations.push_str(if en {
" This run is incomplete: scores describe only the successfully measured scope."
} else {
" Dieser Lauf ist unvollständig: Die Scores beschreiben nur den erfolgreich gemessenen Umfang."
});
}
let disclaimer = if en {
"This report represents an automated technical analysis. It does not replace a complete \
WCAG 2.1 conformance assessment. A legally defensible accessibility statement requires a \
comprehensive manual audit by experts."
.to_string()
} else {
"Dieser Report stellt eine automatisierte technische Analyse dar. \
Er ersetzt keine vollständige Konformitätsbewertung nach WCAG 2.1. \
Für eine rechtsverbindliche Aussage zur Barrierefreiheit ist eine \
umfassende manuelle Prüfung durch Experten erforderlich."
.to_string()
};
let key = |de: &str, en_label: &str| -> String {
if en {
en_label.to_string()
} else {
de.to_string()
}
};
let preview_value = if normalized.has_screenshots {
if en {
"Desktop and mobile captured".to_string()
} else {
"Desktop und Mobile erfasst".to_string()
}
} else if en {
"Not captured".to_string()
} else {
"Nicht erfasst".to_string()
};
let total_score_value = {
let contributing: Vec<_> = normalized
.module_scores
.iter()
.filter(|m| m.contributes_to_overall)
.collect();
let total_raw: u32 = contributing.iter().map(|m| m.weight_pct).sum();
let weights: Vec<String> = contributing
.iter()
.map(|m| {
let pct = (m.weight_pct * 100 + total_raw / 2)
.checked_div(total_raw)
.unwrap_or(0);
let name = localized_module_name(&m.name, i18n);
if en {
format!("{name} {pct}%")
} else {
format!("{name} {pct} %")
}
})
.collect();
let weights_label = if weights.is_empty() {
if en {
"Accessibility 100%".to_string()
} else {
"Barrierefreiheit 100 %".to_string()
}
} else {
weights.join(", ")
};
let indicator_names: Vec<String> = normalized
.module_scores
.iter()
.filter(|m| !m.contributes_to_overall || m.measurement_type == "heuristic")
.map(|m| localized_module_name(&m.name, i18n))
.collect();
let indicator_note = if indicator_names.is_empty() {
String::new()
} else if en {
format!(
" Indicator modules shown separately: {}.",
indicator_names.join(", ")
)
} else {
format!(
" Separat ausgewiesene Indikator-Module: {}.",
indicator_names.join(", ")
)
};
if en {
format!(
"{} / 100 — contributing weights: {}{}",
normalized.overall_score, weights_label, indicator_note
)
} else {
format!(
"{} / 100 — beitragende Gewichtung: {}{}",
normalized.overall_score, weights_label, indicator_note
)
}
};
let runtime_unit = "s";
let accessibility_score_value = normalized
.viewport_scores
.as_ref()
.map(|scores| {
if en {
format!(
"{} / 100 — 70% mobile ({}) + 30% desktop ({})",
normalized.score, scores.mobile.accessibility, scores.desktop.accessibility
)
} else {
format!(
"{} / 100 — 70 % Mobile ({}) + 30 % Desktop ({})",
normalized.score, scores.mobile.accessibility, scores.desktop.accessibility
)
}
})
.unwrap_or_else(|| format!("{} / 100", normalized.score));
let all_occurrences: usize = normalized
.findings
.iter()
.map(|finding| finding.occurrence_count)
.sum();
let counting_value = if en {
format!(
"{} distinct WCAG finding groups, {} WCAG occurrences; {} finding rows and {} occurrences across all categories",
normalized.severity_counts.total,
normalized.occurrence_counts.total,
normalized.findings.len(),
all_occurrences
)
} else {
format!(
"{} unterschiedliche WCAG-Befundgruppen, {} WCAG-Vorkommen; {} Befundzeilen und {} Vorkommen über alle Kategorien",
normalized.severity_counts.total,
normalized.occurrence_counts.total,
normalized.findings.len(),
all_occurrences
)
};
MethodologyBlock {
scope,
method,
limitations,
disclaimer,
audit_facts: vec![
(
key("Barrierefreiheits-Score", "Accessibility score"),
accessibility_score_value,
),
(key("Gesamtscore", "Overall score"), total_score_value),
(key("Zählweise", "Counting method"), counting_value),
(
key("WCAG-Level", "WCAG level"),
normalized.wcag_level.to_string(),
),
(
key("Geprüfte Knoten", "Checked nodes"),
normalized.nodes_analyzed.to_string(),
),
(
key("Laufzeit", "Runtime"),
format!(
"{:.1} {}",
normalized.duration_ms as f64 / 1000.0,
runtime_unit
),
),
(key("Aktive Module", "Active modules"), active_modules),
(
key("Fehlgeschlagene Module", "Failed modules"),
if failed_modules.is_empty() {
key("Keine", "None")
} else {
failed_modules
},
),
(
key("Audit-Qualität", "Audit quality"),
format!("{:?}", normalized.execution.quality.status),
),
(
key("Audit-Hinweise", "Audit notes"),
normalized.audit_flags.len().to_string(),
),
(key("Vorschau", "Preview"), preview_value),
],
confidence_summary: build_confidence_summary(locale, normalized),
capabilities: build_capability_matrix(locale, normalized),
}
}
fn build_confidence_summary(locale: &str, normalized: &NormalizedReport) -> Vec<(String, String)> {
let en = locale == "en";
let base_confidence = if normalized.nodes_analyzed >= 2_000 {
if en {
"High"
} else {
"Hoch"
}
} else if normalized.nodes_analyzed >= 500 {
if en {
"Solid"
} else {
"Solide"
}
} else if en {
"Limited"
} else {
"Begrenzt"
};
let caveat_level = if normalized.audit_flags.is_empty() {
if en {
"No automatically detected conflict signals"
} else {
"Keine automatisiert erkannten Konfliktsignale"
}
} else if normalized.audit_flags.len() == 1 {
if en {
"1 caveat signal"
} else {
"1 Hinweissignal"
}
} else if en {
"Multiple caveat signals"
} else {
"Mehrere Hinweissignale"
};
let module_coverage = if normalized.module_scores.len() >= 5 {
if en {
"Broad"
} else {
"Breit"
}
} else if normalized.module_scores.len() >= 3 {
if en {
"Extended"
} else {
"Erweitert"
}
} else if en {
"Core checks"
} else {
"Kern-Checks"
};
let (label_trust, label_coverage, label_signals, label_manual, val_manual) = if en {
(
"Audit confidence",
"Module coverage",
"Conflict signals",
"Manual review needed",
"Yes, for semantic quality and usage context",
)
} else {
(
"Audit-Vertrauen",
"Modul-Abdeckung",
"Konfliktsignale",
"Manuelle Prüfung nötig",
"Ja, für semantische Qualität und Nutzungskontext",
)
};
vec![
(label_trust.to_string(), base_confidence.to_string()),
(label_coverage.to_string(), module_coverage.to_string()),
(label_signals.to_string(), caveat_level.to_string()),
(label_manual.to_string(), val_manual.to_string()),
]
}
fn build_capability_matrix(locale: &str, normalized: &NormalizedReport) -> Vec<CapabilitySignal> {
let en = locale == "en";
let confidence_high = if en { "High" } else { "Hoch" };
let confidence_solid = if en { "Solid" } else { "Solide" };
let confidence_off = if en { "Not active" } else { "Nicht aktiv" };
let mut capabilities = vec![
CapabilitySignal {
signal: if en {
"WCAG rules & occurrences".into()
} else {
"WCAG-Regeln & Vorkommen".into()
},
source: if en {
"Accessibility tree + rule engine".into()
} else {
"Accessibility Tree + Regelengine".into()
},
confidence: if normalized.nodes_analyzed >= 500 {
confidence_high.to_string()
} else {
confidence_solid.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into(), "Studio".into()],
note: if en {
"Primary audit truth for automatically detectable violations.".to_string()
} else {
"Primäre Audit-Wahrheit für automatisiert erkennbare Verstöße.".to_string()
},
},
CapabilitySignal {
signal: if en {
"Web vitals & loading indicators".into()
} else {
"Web Vitals & Ladeindikatoren".into()
},
source: if en {
"Performance module".into()
} else {
"Performance-Modul".into()
},
confidence: if normalized
.module_scores
.iter()
.any(|m| m.name == "Performance")
{
confidence_high.to_string()
} else {
confidence_off.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into()],
note: if en {
"FCP, CLS and TTFB are reflected in facts and module sections.".to_string()
} else {
"FCP, CLS und TTFB werden in Facts und Modulkapiteln gespiegelt.".to_string()
},
},
CapabilitySignal {
signal: if en {
"SEO structure & schema".into()
} else {
"SEO-Struktur & Schema".into()
},
source: if en {
"SEO module".into()
} else {
"SEO-Modul".into()
},
confidence: if normalized.module_scores.iter().any(|m| m.name == "SEO") {
confidence_solid.to_string()
} else {
confidence_off.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into()],
note: if en {
"Meta, heading and schema signals are condensed into a report-ready form."
.to_string()
} else {
"Meta-, Heading- und Schema-Signale sind reportfähig verdichtet.".to_string()
},
},
CapabilitySignal {
signal: if en {
"Security headers & HTTPS".into()
} else {
"Security Header & HTTPS".into()
},
source: if en {
"Security module".into()
} else {
"Security-Modul".into()
},
confidence: if normalized
.module_scores
.iter()
.any(|m| m.name == "Security")
{
confidence_high.to_string()
} else {
confidence_off.to_string()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into()],
note: if en {
"Header presence and HTTPS status remain visible as raw signal.".to_string()
} else {
"Header-Präsenz und HTTPS-Status bleiben als Rohsignal sichtbar.".to_string()
},
},
CapabilitySignal {
signal: if en {
"Mobile, UX, journey".into()
} else {
"Mobile, UX, Journey".into()
},
source: if en {
"Heuristic modules".into()
} else {
"Heuristik-Module".into()
},
confidence: if en {
"Indicator-based".into()
} else {
"Hinweisbasiert".into()
},
surfaces: vec!["CLI".into(), "JSON".into(), "PDF".into(), "Studio".into()],
note: if en {
"Useful for prioritization, not the sole UX truth.".to_string()
} else {
"Zur Priorisierung geeignet, nicht als alleinige UX-Gesamtwahrheit.".to_string()
},
},
];
if !normalized.audit_flags.is_empty() {
capabilities.push(CapabilitySignal {
signal: if en {
"Audit conflict signals".into()
} else {
"Audit-Konfliktsignale".into()
},
source: if en {
"Normalization / cross-checks".into()
} else {
"Normalisierung / Cross-Checks".into()
},
confidence: if en {
"Explicitly flagged".into()
} else {
"Explizit markiert".into()
},
surfaces: vec!["JSON".into(), "PDF".into()],
note: if en {
format!(
"{} conflict {} surfaced openly rather than hidden in the score.",
normalized.audit_flags.len(),
if normalized.audit_flags.len() == 1 {
"signal is"
} else {
"signals are"
}
)
} else {
format!(
"{} {} offen ausgewiesen statt im Score versteckt.",
normalized.audit_flags.len(),
if normalized.audit_flags.len() == 1 {
"Konfliktsignal wird"
} else {
"Konfliktsignale werden"
}
)
},
});
}
capabilities
}