"""Rendu HTML « Recherchabilité fuzzy » — Sprint 86 (A.II.5a HTML). Suite directe ``picarones/core/searchability.py`` (Sprint 84) + câblage runner (Sprint 86). Pattern identique aux autres rendus (Sprints 41/43/62/67/72) : **server-side**, pas de JavaScript, anti-injection systématique. Vue --- Tableau résumé : moteur × (rappel, n_searchable / n_gt_tokens, docs). Cellule rappel colorée par gradient rouge → vert. Adaptative : ``""`` si aucun moteur n'a de ``aggregated_searchability``. """ from __future__ import annotations from html import escape as _e from typing import Optional from picarones.report.render_helpers import color_traffic_light def build_searchability_summary_html( engines: list[dict], labels: Optional[dict[str, str]] = None, ) -> str: """Construit la table HTML de recherchabilité. Parameters ---------- engines: Liste de dicts moteur ; chacun peut avoir ``aggregated_searchability``. labels: Dict i18n, clés ``search_*``. Returns ------- str ``""`` si aucun moteur n'a de signal. """ rows = [ e for e in engines if isinstance(e.get("aggregated_searchability"), dict) ] if not rows: return "" labels = labels or {} title = labels.get("search_title", "Recherchabilité fuzzy") note = labels.get( "search_note", "Proportion de tokens GT retrouvés dans la sortie OCR à " "distance de Levenshtein ≤ 2 — proxy direct de la " "qualité pour la recherche plein-texte (Elastic, Solr).", ) col_engine = labels.get("search_engine", "Moteur") col_recall = labels.get("search_recall", "Rappel") col_count = labels.get("search_count", "Tokens retrouvés / total") col_docs = labels.get("search_docs", "Docs") parts = [ '
| ' f'{_e(col)} | ' ) parts.append("|||
|---|---|---|---|
| {_e(str(name))} | ' f'' f'{recall * 100:.1f}% | ' f'{n_search} / {n_total} | ' f'{n_docs} | ' f'