"""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(title)}

', f'
' f'{_e(note)}
', '', '', f'', f'', ] for cat in visible_cats: parts.append( f'' ) parts.append("") for engine in rows: agg = engine["aggregated_numerical_sequences"] name = engine.get("name") or "?" per_cat = agg.get("per_category") or {} global_strict = float(agg.get("global_strict_score") or 0.0) global_value = float(agg.get("global_value_score") or 0.0) n_total = int(agg.get("n_total") or 0) global_color = color_traffic_light(global_strict) parts.append( f'' f'' f'' ) 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'' ) parts.append("") parts.append("
' f'{_e(col_engine)}' f'{_e(col_global)}' f'{_e(cat_label.get(cat, cat))}
{_e(str(name))}' f'{global_strict * 100:.1f}%' f' ({global_value * 100:.0f}%, ' f'n={n_total})' f'{strict * 100:.0f}%' f' ' f'({value * 100:.0f}%, n={n})
") return "".join(parts) __all__ = ["build_numerical_sequences_html"]