"""Rendu HTML server-side de la vue stratifiée par script_type (Sprint 46). Suite directe du Sprint 45 (couche backend). Affiche le classement moteur par strate sous forme de tableaux pliables (HTML ``
``, pas de JavaScript). - ``build_stratified_ranking_html`` — un ``
`` par strate avec tableau ``moteur, médiane, moyenne, docs``. Cellule médiane colorée par gradient vert (faible CER) → rouge (CER élevé). Principe : cohérent avec ``inter_engine_render``, ``ner_render`` et ``calibration_render`` — server-side, déterministe, pas de JS. Masquage adaptatif : la fonction retourne ``""`` si aucune strate n'est disponible (``available_strata`` vide). Anti-injection : tous les noms de moteurs et de strates sont passés à ``html.escape``. """ from __future__ import annotations from html import escape as _e from typing import Optional from picarones.report.render_helpers import color_traffic_light def _format_cer(cer: Optional[float]) -> str: if cer is None: return "—" return f"{cer * 100:.2f} %" def build_stratified_ranking_html( stratified_ranking: Optional[dict], available_strata: Optional[list], homogeneity: Optional[dict] = None, labels: Optional[dict[str, str]] = None, ) -> str: """Construit la section HTML stratifiée. Parameters ---------- stratified_ranking: ``{stratum: [ranking_entry, …]}`` produit par ``BenchmarkResult.stratified_ranking()``. available_strata: Liste triée des strates (``BenchmarkResult.available_strata()``). homogeneity: Dict produit par ``BenchmarkResult.corpus_homogeneity()`` si disponible — sert à afficher l'écart inter-strate du leader en tête de section. labels: i18n. Fallback FR si manquantes. Returns ------- str HTML ``
...
`` ou ``""`` si stratification absente. """ if not stratified_ranking or not available_strata: return "" labels = labels or {} caption = labels.get( "stratification_caption", "Classement par strate (script_type)", ) description = labels.get( "stratification_description", "Le tableau global classe sur l'ensemble du corpus. Quand le " "corpus est hétérogène, certains moteurs dominent sur un type " "de document et perdent sur un autre — la vue stratifiée le " "révèle.", ) engine_label = labels.get("col_engine", "Moteur") median_label = labels.get("stratification_median_label", "Médiane CER") mean_label = labels.get("stratification_mean_label", "Moyenne CER") docs_label = labels.get("stratification_docs_label", "Documents") no_data = labels.get("stratification_no_data_label", "—") n_docs_in_stratum_label = labels.get( "stratification_n_docs_label", "documents", ) parts: list[str] = [] parts.append('
') parts.append( f'

{_e(caption)}

' ) parts.append( f'
{_e(description)}
' ) # Bandeau d'hétérogénéité si disponible if homogeneity and homogeneity.get("max_inter_strata_gap") is not None: gap = float(homogeneity["max_inter_strata_gap"]) leader = str(homogeneity.get("leader") or "") min_strat, max_strat = homogeneity.get( "leader_max_gap_strata", ["", ""] ) gap_template = labels.get( "stratification_gap_summary", "Écart inter-strate du leader {leader} : {gap_pct} points " "de CER médian (entre « {min_stratum} » et « {max_stratum} »).", ) gap_text = gap_template.format( leader=leader, gap_pct=f"{gap * 100:.1f}", min_stratum=min_strat, max_stratum=max_strat, ) # gap_text contient déjà des données utilisateur — on n'échappe pas # le template lui-même (i18n connue), mais on n'injecte pas non plus # de markup. _e() est appliqué aux variables via format() côté template. parts.append( f'
⚠ {_e(gap_text)}
' ) # Une ``
`` par strate (premier ouvert pour donner le contexte) for i, stratum in enumerate(available_strata): entries = stratified_ranking.get(stratum) or [] n_docs_total = max((int(e.get("documents") or 0) for e in entries), default=0) open_attr = " open" if i == 0 else "" parts.append( f'
' ) parts.append( f'' f'{_e(stratum)} ' f'({n_docs_total} {_e(n_docs_in_stratum_label)})' f'' ) parts.append( '' ) parts.append("") for hdr in (engine_label, median_label, mean_label, docs_label): parts.append( f'' ) parts.append("") for entry in entries: engine = str(entry.get("engine", "")) median = entry.get("median_cer") mean = entry.get("mean_cer") n_docs = int(entry.get("documents") or 0) bg = color_traffic_light(float(median), low_is_good=True, scale_max=0.30) if median is not None else "#f4f4f4" parts.append("") parts.append( f'' ) parts.append( f'' ) parts.append( f'' ) parts.append( f'' ) parts.append("") parts.append("
' f'{_e(hdr)}
' f'{_e(engine)}' f'{_e(_format_cer(median)) if median is not None else _e(no_data)}' f'' f'{_e(_format_cer(mean)) if mean is not None else _e(no_data)}' f'{n_docs}
") parts.append("
") parts.append("
") return "".join(parts) __all__ = [ "build_stratified_ranking_html", ]