"""Rendu HTML « Précision sur séquences numériques » — Sprint 86. Suite directe ``picarones/core/numerical_sequences.py`` (Sprint 85) + câblage runner Sprint 86. Pattern identique aux autres rendus : server-side, pas de JS, anti-injection systématique. Vue --- Tableau moteur × catégorie (year / roman / foliation / currency / regnal) × score strict ; une ligne par moteur, une cellule colorée par cellule. Une seconde ligne donne le score ``value`` (en plus petit). Catégorie omise si **aucun** moteur n'a de GT exploitable pour elle. Adaptative : ``""`` si aucun moteur n'a de ``aggregated_numerical_sequences``. """ from __future__ import annotations from html import escape as _e from typing import Optional from picarones.measurements.numerical_sequences import CATEGORIES from picarones.report.render_helpers import color_traffic_light def _category_columns_with_signal(rows: list[dict]) -> list[str]: """Ne garde que les catégories où ≥ 1 moteur a un n_total > 0.""" visible: list[str] = [] for cat in CATEGORIES: for r in rows: agg = r.get("aggregated_numerical_sequences") or {} cat_data = (agg.get("per_category") or {}).get(cat) or {} if (cat_data.get("n_total") or 0) > 0: visible.append(cat) break return visible def build_numerical_sequences_html( engines: list[dict], labels: Optional[dict[str, str]] = None, ) -> str: """Construit la section HTML séquences numériques. Returns ------- str ``""`` si aucun moteur n'a de signal. """ rows = [ e for e in engines if isinstance(e.get("aggregated_numerical_sequences"), dict) ] if not rows: return "" visible_cats = _category_columns_with_signal(rows) if not visible_cats: return "" labels = labels or {} title = labels.get( "numseq_title", "Précision sur séquences numériques", ) note = labels.get( "numseq_note", "Score strict (forme préservée) — la valeur entre " "parenthèses est le score sur la valeur (XIV ↔ 14 " "accepté). Foliotation : recto/verso non interchangeables.", ) col_engine = labels.get("numseq_engine", "Moteur") col_global = labels.get("numseq_global", "Global") cat_label = { "year": labels.get("numseq_cat_year", "Année"), "roman": labels.get("numseq_cat_roman", "Romain"), "foliation": labels.get("numseq_cat_foliation", "Foliation"), "currency": labels.get("numseq_cat_currency", "Montant"), "regnal": labels.get("numseq_cat_regnal", "Régnal"), } parts = [ '
| ' f'{_e(col_engine)} | ', f'' f'{_e(col_global)} | ', ] for cat in visible_cats: parts.append( f'' f'{_e(cat_label.get(cat, cat))} | ' ) parts.append("|
|---|---|---|---|
| {_e(str(name))} | ' f'' f'{global_strict * 100:.1f}%' f' ({global_value * 100:.0f}%, ' f'n={n_total}) | ' ) for cat in visible_cats: cat_data = per_cat.get(cat) or {} n = int(cat_data.get("n_total") or 0) if n == 0: parts.append( '— | ' ) continue strict = float(cat_data.get("strict_score") or 0.0) value = float(cat_data.get("value_score") or 0.0) color = color_traffic_light(strict) parts.append( f'' f'{strict * 100:.0f}%' f' ' f'({value * 100:.0f}%, n={n}) | ' ) parts.append("