# Changelog — Picarones Tous les changements notables de ce projet sont documentés dans ce fichier. Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/). La numérotation de version suit [Semantic Versioning](https://semver.org/lang/fr/). Politique de versionning : voir [`docs/explanation/versioning.md`](docs/explanation/versioning.md). --- ## Note de repositionnement (2026-05-23) Le projet a été repositionné en **SemVer pré-1.0** à cette date. La release précédemment dénommée **« 2.0.0 »** (clôture du rewrite architectural en 8 couches, mai 2026) est désormais référencée comme **`0.9.0`** dans ce changelog. Justification : à la clôture du rewrite, le projet n'avait ni interface utilisateur finalisée, ni parité importeurs, ni rapport refondu — la dénomination « 2.0 » reflétait un cycle de refactor interne, pas une release publique mûre. La sortie de `1.0.0` deviendra un événement public marquant à la livraison de ces chantiers. Les sections historiques de ce fichier (et de la doc archive) qui citent « v2.0 » ou « v1.x » dans leur **prose** n'ont pas été réécrites : modifier le passé pour faire coïncider la terminologie avec le présent serait une réécriture d'historique trompeuse. Lis « v2.0 » dans ces sections comme « l'état du code à la clôture du rewrite, devenu `0.9.0` au repositionnement ». Seuls les **titres de version** (`## [X.Y.Z]`) ont été réalignés pour que les tags git et les releases PyPI futures restent cohérents avec le numéro courant. --- ## Roadmap vers 1.0.0 La sortie de `1.0.0` est conditionnée à la livraison de : 1. **Surface UI complète** — exposition de tous les champs du modèle `BenchmarkRunRequest`, parité fonctionnelle avec la CLI (`compare`, `robustness`, `history`). 2. **Parité importeurs corpus** — IIIF, Gallica, eScriptorium accessibles depuis l'UI web. 3. **Refonte rapport HTML** — IA 4 onglets (Overview / Engines / Documents / Crosses). Les versions intermédiaires `0.10.0`, `0.11.0`, etc. publient des jalons techniques au fil du chantier. --- ## Lecture chronologique du fichier Les **trois sections `[Unreleased]`** ci-dessous (« Migration Option B », « Audit code-quality », « Chantier post-rewrite ») documentent des chantiers parallèles réalisés **pendant la fenêtre du rewrite** (mai 2026), dont l'aboutissement est consigné dans la section ``[0.9.0] — Legacy retirement complete`` plus bas. Elles ont été préservées sous forme `[Unreleased]` pour refléter l'état où chaque branche a été rédigée — chaque branche était alors un chantier distinct avant que le rewrite ne les absorbe toutes. À partir de la release `0.9.0`, le CHANGELOG suit Keep-a-Changelog strict : **une seule section `[Unreleased]` à la fois** au-dessus de la version courante. Ordre de lecture chronologique : 1. ``[0.9.0] — Legacy retirement complete (mai 2026)`` — aboutissement. 2. ``[Unreleased] — Migration Option B`` — sous-chantier intégré. 3. ``[Unreleased] — Audit code-quality`` — sous-chantier intégré. 4. ``[Unreleased] — Chantier post-rewrite`` — sous-chantier intégré. --- ## [Unreleased] — Retrait de `execution_mode` (mai 2026) Branche `claude/focused-turing-2eFHt`. ### Removed (breaking, surface adapter) - L'attribut de classe ``execution_mode`` est retiré de tous les adapters (``BaseOCRAdapter``, ``BaseLLMAdapter``, ``BaseModule``) ainsi que le type ``ExecutionMode`` (``picarones.pipeline.protocols``, ``picarones.domain.module_protocol``). Aucun consommateur n'en dépendait — l'attribut prétendait depuis longtemps influencer le pool d'exécution du ``CorpusRunner`` mais le runner est thread-only par conception et n'a jamais lu ce champ. - Le type ``ExecutionMode`` n'est plus ré-exporté par ``picarones.pipeline``. ### Changed (documentation) - ``picarones/pipeline/runner.py`` : la docstring assume désormais explicitement le choix thread-only. Tous les adapters supportés (OCR via binaire C, LLM/VLM via httpx, ML via PyTorch/TensorFlow) délèguent leur travail bloquant à du code natif qui relâche le GIL, donc un thread pool unique donne les performances attendues. - ``docs/reference/specification.md`` et ``docs/reference/api-stable.md`` réconciliés avec l'arborescence canonique 0.9.0. - ``README.md``, ``CLAUDE.md``, ``docs/developer/module-policy.md`` mis à jour. ### Added - ``tests/architecture/test_no_execution_mode_resurrection.py`` — bloque la réintroduction silencieuse de l'attribut. Le test pointe vers le rationale et l'historique git (commits ``047ab1b`` et ``cd67184``) en cas d'échec. ### Pourquoi L'attribut était un mensonge structurel. L'historique git portait une tentative de dispatch multi-pool (``MultiDomainCorpusRunner``, commit ``047ab1b``) qui a été suspendue (``cd67184``). Aucun adapter actuel n'est GIL-bound en pratique — chacun délègue à du C qui relâche le GIL. Retirer plutôt que documenter un attribut zombi : YAGNI. --- ## [Unreleased] — Chantier UI + refonte rapport XerOCR (mai 2026) Branche `claude/charming-ritchie-Z820A` — chantier 17 commits qui attaque les trois prérequis annoncés de `1.0.0` (surface UI complète, parité importeurs, refonte rapport). À mi-parcours : refonte rapport et importeurs livrés, surface UI partiellement étendue. ### Versioning — repositionnement SemVer pré-1.0 - ``0.9.0`` officialisé (auparavant la sortie « 2.0 » du rewrite a été renumérotée — cf. « Note de repositionnement » ci-dessus). - ``picarones/domain/_version_fallback.py`` introduit comme unique source de la version (``FALLBACK_VERSION = "0.9.0"``) — garde-fou par ``tests/architecture/test_single_version_source.py`` et ``test_no_hardcoded_version.py``. - Politique versioning documentée ([`docs/explanation/versioning.md`](docs/explanation/versioning.md)). ### Track APP — UI web (sprints S1-S4) - **S1 — Exposition de 6 toggles** de ``BenchmarkRunRequest`` dans l'UI (was : champs Pydantic ignorés par le frontend) : Wilcoxon, Friedman, bootstrap, robustness, NER extractor, historique opt-in. - **S2 — Importeurs IIIF + Gallica BnF** dans la vue Import : preview avant import, recherche SRU Gallica avec critères combinables, sélecteur de pages. - **S3 — Importeur eScriptorium** + catalogue HTR (Kraken/Calamari) : endpoint POST avec token API non loggé, validation HTTPS. - **S4 — Vue Historique longitudinal** : SQLite ``BenchmarkHistory``, sparklines SVG vanilla, table filtrable, détection des régressions via API REST dédiée. ### Track REPORT — refonte rapport HTML XerOCR (sprints S5-S12) - **S5a — Fondation visuelle XerOCR** : ``_design_tokens.css`` partagé avec l'app (palette warm paper + halftone Xerox Star + IBM Plex + Bricolage Grotesque + accents oklch ``fern/slate/clay/ butter``), guard-rail ``test_xerocr_tokens.py`` qui vérifie la parité tokens app↔rapport (42 assertions). - **S5b — Squelette nouvelle IA 4 vues + routeur hash** : ``_routing.js`` (vues XerOCR + sous-onglets ``engines/{table, stability, diagnostics}`` + deeplinks ``#`` + roving tabindex + navigation clavier ←/→/Home/End). - **S6 — Vue Overview** : hero stats + corpus card + ranking synthèse + narrative + diagramme de différence critique. - **S7 — Vue Engines/Tableau** : tableau de référence avec super-headers groupés par catégorie sémantique (clay/slate/ butter/fern), badges A→E (lettres + accents cycliques), mini-barres CSS, tri client-side. - **S8 — Vue Engines/Stabilité** : composite multirun + longitudinal + baseline + robustness_projection avec empty state pédagogique (2 prérequis listés). - **S9 — Vue Engines/Diagnostics** : 7 sections (levers, taxonomy, calibration, philological, NER, over-normalization, numerical_sequences), bloc Sur-normalisation lexicale dédié. - **S10 — Vue Documents** : galerie avec aperçus de strate synthétiques CSS (presse / imprimé / manuscrit / défaut), filtres par chip, drill-in délégué à la vue legacy, branchement des « lignes les plus problématiques » globales en bas. - **S11 — Vue Croisements** : inter-moteurs (divergence + oracle gap) + 4 scatters SVG vanilla (CER × Coût · CER × Gini · Ancrage × Longueur · CER × Durée, accents butter/slate/clay/fern) + spécialisation + comparaison taxonomique. - **S11b — Retrait wrapper ``
`` legacy** : passage à un container neutre ``legacy-views-container``, drill-in préservé, deeplinks legacy ``#ranking/#gallery/...`` préservés. - **S12 — Compare 2 runs + footer manifest + a11y polish** : comparaison de runs client-side (FileReader → delta CER par moteur → bandeau sticky avec régressions/améliorations, 0 appel réseau), footer manifest 3 zones (brand · manifest · actions), ``aria-current="page"`` + roving tabindex + focus rings visibles. ### Sprint S13 — Audit institutionnel + corrections critiques Audit à 4 angles (backend / JS+CSS / renderers Python / i18n+ tests+runtime) — 59 issues identifiées (12 critiques + 29 majeures + 18 mineures). Corrections appliquées par phases : - **Phase 1 — 10 critiques UX/i18n/cohérence** : 11 clés ``data-i18n`` manquantes ajoutées (``tab_*``, ``overview_*``, ``engines_*``, ``engines_sub_*``) ; 4 eyebrows + 5 aria-labels hardcodés FR migrés vers ``data-i18n-attr-aria-label`` ; 4 ``

`` regroupés en 1 seul (header) + 4 ``

`` (vues) ; ``engines-hero-stats`` peuplé (3 stats moteurs/pipelines/ métriques) ; ``_normalize_strate`` étendu à 20 termes paléographiques (gothique/humanistique/cursive/caroline/onciale/ bâtarde/textura/rotunda/fraktur/anglicana/secretary/chancery…) ; ``_switchView`` ne casse plus la nav XerOCR au drill-in ; Overview ↔ Documents lisent désormais la même source pour les strates ; hero scatter counter aligné sur ``isinstance`` ; modèles Anthropic fallback corrigés (modèles inventés retirés) ; triple duplication ``_ENGINE_ACCENTS`` factorisée dans ``picarones/reports/_helpers/engine_badges.py`` ; ``esc()`` legacy échappe désormais l'apostrophe (XSS via ``onclick='...'``) ; NORM par défaut affiche ``—`` au lieu de ``"nfc"`` fabriqué. - **Phase 2 — sécurité S2/S3 (C1 + M3)** : nouveau helper ``_import_guards.py`` qui applique ``state.enforce_rate_limit`` systématiquement sur ``iiif/gallica/escriptorium`` ; en mode public, plafonne ``max_resolution`` à 2048 px et refuse ``pages="all"`` ; nouveau ``_RevalidatingRedirectHandler`` dans ``_http.py`` qui re-valide chaque redirect HTTP (anti-SSRF post-redirect, parade AWS metadata). - **Phase 3 — polish + tests** : ``BenchmarkHistory`` passe en WAL mode + retry 30s (concurrence robuste) ; ``_compare.js`` ajoute MAX_BYTES 50 Mo + `Number.isFinite` (anti-NaN) + ``label_other`` dead code retiré ; renderers ``data-value`` échappés ; ``name=None`` → ``"—"`` au lieu de ``"None"`` ; ``worst_lines`` utilise ``

`` sémantique ; ``oklch()`` reçoit fallbacks ``#hex`` (Chrome <111 / Firefox <113 / Safari <15.4) ; ``test_renderer_exception_logged_not_raised`` teste effectivement le log via ``caplog`` ; fichier sprint-nommé renommé ; ``tests/reports/conftest.py`` centralise le fixture ``demo_html`` scope session. DoD : - 5560+ tests passent, 0 failed. - ``ruff check`` propre. - 4 audits indépendants : verdict « niveau institutionnel atteint » sur la majorité des dimensions ; reste 18 mineures documentées (dette technique). ### Audit XerOCR-2 — 9 phases de remédiation (mai 2026) Second tour d'audit après S13. Chaque phase isole un thème, mesure son impact via tests, et verrouille le résultat (les tests deviennent les contrats anti-régression). Cibles attaquées : bugs backend → frontend non exploités, qualité du rendu, fluidité, éléments graphiques, et code legacy dormant. - **Phase 1 — Reproductibilité scientifique** : - ``engine_config`` (modèle / température / prompt) propagé de ``EngineReport`` vers ``engines_summary`` puis affiché en ``
`` sous le nom de chaque moteur dans le tableau Engines — sans cela l'utilisateur ne voyait pas avec quels paramètres un CER avait été obtenu, contournant la reproductibilité. - 2 nouvelles colonnes ``μCER`` / ``μWER`` (métriques micro canoniques, indispensables pour les corpus déséquilibrés) réintroduites dans le tableau Engines + super-headers « ERREUR · CARACTÈRE/MOT » étendus. - ``overview.py:_resolve_normalization_label`` lisait ``snapshots.normalization_profile`` qui n'est jamais produit ; clé canonique ``snapshots.normalization`` rétablie — le snapshot autoritatif n'était plus silencieusement ignoré avec fallback systématique sur ``meta.metadata``. - **Phase 2 — Vue Stabilité fonctionnelle depuis SQLite history** : - Nouveau module ``picarones/reports/html/data/history.py`` (306 LOC) qui pont la SQLite ``BenchmarkHistory`` vers ``report_data`` — alimente ``longitudinal`` + ``baseline_difficulty`` + ``baseline_comparisons`` (consommé par le détecteur narratif ``engine_off_baseline``) + ``longitudinal_trends`` (alias consommé par ``regression_in_history``). - ``ReportGenerator`` accepte ``history=`` ou ``history_db_path=`` (défaut env var ``PICARONES_HISTORY_DB`` puis ``~/.picarones/history.db``). - Empty state composite Stabilité refactoré : retourne ``""`` quand les 4 sources internes sont vides, exposé séparément via ``build_stability_empty_state_html``. Le template arbitre : empty state UNIQUEMENT si composite + Wilcoxon + CER-dist sont tous silencieux — sinon le message « pas de données » contredisait visuellement les tests statistiques effectifs rendus juste en dessous. - **Phase 3 — taxonomy_comparison + cohérence scatter Crosses** : - Le composite Crosses lisait ``report_data["taxonomy_comparison"]`` qui n'était jamais produit — le bloc « comparaison taxonomique leader vs runner-up » apparaissait silencieusement vide. Nouveau ``compute_taxonomy_comparison_section`` dans ``data/extra_metrics.py``. - Hero stat Crosses comptait les scatters avec critère ``>= 2`` points mais ``_build_cer_cost_scatter`` rendait dès ``>= 1`` : hero disait « 0 scatters » mais un scatter dégénéré avec 1 point s'affichait quand même. Alignement strict ``isinstance + >= 2`` partout. - **Phase 4 — Galerie Documents + SVG aspectRatio** : - Badge difficulté intrinsèque (Facile / Modéré / Difficile / Très difficile, seuils 0.25 / 0.50 / 0.75 sur ``difficulty_score``) désormais affiché sur chaque carte de la galerie — le score était calculé par ``annotate_documents_with_difficulty`` mais jamais rendu. Couleur Okabe-Ito (colorblind-friendly). - Empty state Documents promu pédagogique : titre + body + 3 bullets (corpus, ground truth, images) — pattern aligné sur Stability / Diagnostics. - ``preserveAspectRatio="xMidYMid meet"`` ajouté aux 4 SVG entry points (helper ``svg_open`` + calibration + pipeline_dag + crosses scatters) — sans cet attribut, ``height:auto`` sur écran étroit écrasait verticalement les charts. - **Phase 5 — Retrait de 310 LOC de JS dormant** : - 6 fonctions Chart.js (``buildCerHistogram``, ``buildRadar``, ``buildVennDiagram``, ``buildWilcoxonTable``, ``buildGiniCerScatter``, ``buildRatioAnchorScatter``) ciblaient des canvas migrés vers SVG côté serveur en S7-S11. Les fonctions persistaient (parsed + JIT-compilées à chaque ouverture de rapport) grâce au try/catch d'orchestration de ``buildCharts()`` qui les laissait échouer silencieusement. ``_app.js`` : 2880 → 2570 LOC (–10,8 %). - 2 clés i18n orphelines retirées (``no_anchor_data``, ``no_gini``) — utilisées uniquement par les fonctions supprimées. - **Phase 6 — A11y clavier Overview + i18n normalization** : - Lignes ranking Overview avaient ``role="link" tabindex="0"`` sans gestionnaire keydown — inactivables au clavier alors qu'annoncées comme links. ``onkeydown`` ajouté pour Enter + Espace avec ``preventDefault``. - Fallback ``"(par défaut)"`` du label normalization devient i18n-aware via ``labels["overview_normalization_default"]``. - **Phase 7 — Couleurs scatters cohérentes + a11y Pareto + responsive** : - ``_build_scatter_svg`` attribuait la couleur des cercles via ``enumerate(cleaned)`` — index dans le tableau de points, pas rang du moteur dans le ranking CER. Le leader pouvait apparaître jaune dans Overview et vert dans le scatter Crosses selon l'ordre d'arrivée de ses données. Helper ``_engine_rank_map(report_data)`` thread le rang depuis ``report_data["ranking"]`` jusqu'à l'attribution de couleur — cohérence inter-tabs verrouillée. - Toggles Pareto (cost / speed / co2) gagnent ``aria-pressed`` + ``role="group"`` sur le toolbar — WCAG 2.1 AA fix (4.1.2 Name, Role, Value). ``setParetoAxis`` synchronise ``aria-pressed`` en plus de la classe CSS ``.active``. - Premier bloc ``@media (max-width: 600px)`` : hero stats en colonne, table Engines fluide, galerie Documents 140px min, scatters Crosses 1 colonne, bandeau compare wrap. - **Phase 8 — Pareto pipelines distincts + axes vides désactivés** : - Pipelines OCR+LLM rendaient ``pointStyle: 'circle'`` comme les moteurs natifs sur le Pareto — indistinguables visuellement. ``pointStyle: 'triangle'`` pour les pipelines (convention reprise des anciens scatters SVG retirés en Phase 5) + tooltip enrichi ``[pipeline]``. - ``_initParetoToggleAvailability()`` invoqué au boot désactive les toggles dont l'axe (speed / co2) n'a pas de données dans ``DATA.pareto`` — empêche un clic qui ouvrirait un canvas vide « Données insuffisantes ». - **Phase 9 — Image CLS + Escape compare + corpus mismatch** : - ```` Documents gallery sans ``width``/``height`` → CLS au premier paint ; sans ``onerror`` → icône « image broken » sur 404. Ajout des 3 attributs (width="200" height="267" pour aspect-ratio 3/4 + onerror display:none). - Bandeau compare non fermable au clavier — nouveau helper ``_wireBannerDismiss`` câble click + Escape avec auto-désinscription via flag ``_disposed``. - ``_detectCorpusMismatch`` émet un préfixe ⚠ + chip ``.xer-compare-warning`` quand les 2 runs comparés sont sur des corpus différents — l'utilisateur lit alors les deltas avec l'œil critique requis (mais peut comparer cross-corpus volontairement, la comparaison n'est pas bloquée). - Docstring de ``engine_badges.py`` récrite avec contrat explicite « ``idx`` = rang dans ``report_data["ranking"]`` (CER ascendant) » + warnings anti-patterns (ne PAS passer ``enumerate(engines)``, position alphabétique, ou tri utilisateur). Garde-fou contre la réintroduction en boucle du bug Phase 7 A1. DoD du chantier (9 phases) : - 5636 tests passent, 0 failed (vs 5560 avant — 76 nouveaux tests qui verrouillent les contrats anti-régression). - 310 LOC de JS dormant supprimées (``_app.js``). - 4 nouveaux modules / helpers (``data/history.py``, ``compute_taxonomy_comparison_section``, ``_engine_rank_map``, ``_difficulty_badge_html``, ``_wireBannerDismiss``). - Parité i18n FR ↔ EN maintenue (634 clés chacun). - ``ruff check`` propre sur tous les commits. ### Audit XerOCR-2bis — re-audit adversarial + Phase 16+ (mai 2026) Un re-audit adversarial post-Phase 15 a remonté 24 findings (1 CRITICAL, 11 HIGH, 8 MED, 4 LOW) qui ont prouvé que certains "fixes" des Phases 1-15 étaient incomplets ou faux. Les phases suivantes traitent ces findings par paquets cohérents avec preuve anti-theater (chaque test ajouté doit échouer sans le fix correspondant). - **Phase 16 — Bugs locaux urgents** : - **[1.1]** ``_initParetoToggleAvailability`` (``_app.js``) hardcodait ``if (axis === 'cost') return;`` — laissait le toggle cost cliquable même sans données pricing. Branche spéciale supprimée, traitement uniforme des 3 axes. - **[1.2]** ``_wireBannerDismiss`` (``_compare.js``) accumulait des handlers ``keydown`` sur ``document`` à chaque comparaison successive (le flag ``_disposed`` ne désinscrivait l'ancien handler qu'au prochain keydown — jamais si l'utilisateur ne tapait plus). Singleton module-level ``_currentKeyHandler`` qui désinscrit explicitement avant d'attacher un nouveau listener. - **[1.4]** ``_open_history`` (``data/history.py``) ouvrait une connexion SQLite sans jamais la fermer. Pattern batch (génération de N rapports) accumulait N handles ouverts sur le même ``.db`` jusqu'au GC + verrouillait le fichier sous Windows. Refactor en ``(history, owned)`` tuple — ``build_history_sections`` ferme la connexion en ``finally`` UNIQUEMENT quand elle l'a ouverte elle-même (préserve les fixtures partagées des tests). - **[5.1]** Seuils difficulté ``0.25 / 0.50 / 0.75`` dupliqués dans 3 fichiers (``evaluation/metrics/difficulty.py``, ``renderers/difficulty.py``, ``renderers/documents_gallery.py``). Nouveau ``difficulty_bucket(score) → str`` dans le module évaluation = source unique de vérité. ``difficulty_label``, ``difficulty_color`` et ``_difficulty_badge_html`` consomment désormais ce helper plutôt que ré-implémenter le bucketing. Changer un seuil ne demande plus 3 modifications parallèles. - **[5.2]** ``row.get("engine", "—")`` (``overview.py``) retournait ``None`` quand la clé existait avec valeur ``None``, ce qui affichait ``"None"`` littéral dans le ranking card. ``engines_table.py`` avait déjà documenté le piège ; pattern aligné ``row.get("engine") or "—"`` partout. - **Phase 16-bis — corrections post re-audit Phase 16** : Un re-audit ciblé sur les 3 commits Phase 16 a remonté 2 fixes incomplets (un partiel + un theater). Phase 16-bis les corrige réellement. - **[1.2] race condition résiduelle** : le singleton module-level ``_currentKeyHandler`` introduisait une fenêtre théorique où un keydown qui arrivait entre ``removeEventListener(old)`` et la réassignation pouvait lire la mauvaise valeur. Handler stocké désormais directement sur l'élément DOM ``banner._keyHandler`` — pas de partage entre instances, séquence strictement locale. - **[5.1] theater côté JavaScript** : la Phase 16 avait migré le bucketing difficulté côté Python (``difficulty_bucket`` comme source unique) mais avait oublié 3 copies du même bucketing dans ``_app.js`` (lignes 912-913, 969). Un changement de seuil Python aurait laissé le JS aux anciens seuils — drift silencieuse. Le dict document expose désormais ``difficulty_slug`` annoté par Python ; le JS le lit directement, ne calcule plus rien. Nouveau test pytest qui grep le JS pour interdire toute réintroduction de bucketing. - **Phase 17 — Tests theater retissés** : Le re-audit adversarial avait flaggé plusieurs tests Phase 1-15 comme « theater » : ils vérifiaient la présence de chaînes de caractères inertes (souvent dans du CSS inliné) plutôt que le comportement. Un test qui passe sous sabotage du code n'est pas un test, c'est une cérémonie. - **[2.1]** ``test_difficulty_badge_present_on_demo_cards`` cherchait ``"doc-card-difficulty"`` ; cette chaîne apparaît aussi dans la définition CSS ``.doc-card-difficulty {`` inlinée via ``{% include '_styles.css' %}``. Le test passait même si le renderer retournait ``""`` pour tous les docs (vérifié par sabotage temporaire). Pattern strict ```` inline. Trou critique du guard découvert --------------------------------- En écrivant le sabotage anti-theater pour [3.5], j'ai découvert que ``_find_isolated_hex`` strippait via regex TOUS les triple-quoted strings, y compris les string literals contenant du CSS. Conséquence : un hex isolé à l'intérieur de ``_INLINE_CSS = """..."""`` était invisible au guard — l'ancien test passait même sans aucune migration. Fix du guard ------------ ``_find_isolated_hex`` utilise désormais ``ast.parse`` pour identifier précisément les lignes qui appartiennent à des docstrings réels (module / fonction / classe). Les string literals assignés à une variable (cas ``_INLINE_CSS``) ne sont PAS masqués — leur contenu reste sous surveillance. Pour les fichiers non-Python (HTML, Jinja2), l'AST échoue et on retombe sur set vide — aucun strip particulier, tous les hex doivent passer le guard tel quel (cohérent avec le comportement attendu pour des templates). Migrations effectives --------------------- - **[3.5]** ``render.py:_INLINE_CSS`` : 20 hex inline (``#222``, ``#444``, ``#ccc``, ``#f4f4f4``, ``#888``, ``#fafafa``, ``#fff8e1``, ``#f9a825``, ``#555``, ``#777``) migrés vers ``var(--ink, ...)``, ``var(--g-700, ...)``, ``var(--g-200, ...)``, ``var(--g-50, ...)``, ``var(--g-500, ...)``, ``var(--paper, ...)``, ``var(--butter-soft, ...)``, ``var(--butter-deep, ...)``, ``var(--g-600, ...)``. - **Bonus** : ``comparison.py`` (CSS inline pour rapport de comparaison de runs) : 15 hex migrés vers la même palette warm paper + ``--fern`` / ``--clay`` / ``--fern-soft`` / ``--clay-soft`` / ``--fern-deep`` / ``--clay-deep``. Guard étendu : auto-discovery ``REPORTS_HTML_DIR.glob("*.py")`` ajoute les 5 modules top-level (``render.py``, ``generator.py``, ``snapshot.py``, ``section_registry.py``, ``comparison.py``) à la couverture. ``_EXEMPTED_TOP_LEVEL_PY`` ne contient que ``__init__.py``. Preuve anti-theater ------------------- Sabotage de ``var(--g-200, #ccc)`` → ``#ccc`` autonome à l'intérieur de ``_INLINE_CSS`` dans ``render.py``. Avant le fix du guard : test passait silencieusement (theater). Après : test échoue sur ``ligne 142 : #ccc`` avec assertion exacte. Restauration → 59/59 verts (37 renderers + 17 templates + 5 top-level py). - **Phase 18c — hex → tokens dans ``_styles.css``** : Troisième morceau du chantier migration CSS, le plus volumineux jusqu'ici : **90 hex isolés** dans ``_styles.css`` migrés vers des fallbacks tokenisés. Trois durcissements du guard découverts en chemin -------------------------------------------------- Comme en Phase 18b, le travail de migration a exposé des faiblesses du guard de Phase 17-18b qu'il a fallu corriger : 1. **Définitions de tokens hors ``:root``** : les blocs ``body.palette-classic`` et ``body:not(.palette-classic)`` définissent ``--palette-good/warning/bad`` avec les hex Okabe-Ito. Ces hex sont des sources de palette, pas des couleurs autonomes. Guard accepte désormais le pattern ``--token-name:`` immédiatement avant un hex (cas (b) du filtrage). 2. **Commentaires CSS ``/* ... */``** : peuvent contenir des hex explicatifs (``/* #EBE8E0 warm paper */``) qui ne sont pas des couleurs actives. Guard strippe désormais les commentaires CSS en passe préalable (en préservant la numérotation de ligne). 3. **Data URLs ``data:image/svg+xml``** : leurs ``%23abc123`` URL-encoded contiennent des hex légitimes (contenu d'image SVG inliné, pas une couleur CSS). Guard ignore les lignes contenant ``data:image/``. Sans ces durcissements, ma migration aurait été en partie ``theater`` : les hex dans les blocs de tokens palette alternative seraient flaggés comme erreurs, et les hex dans les commentaires explicatifs aussi. Migrations effectives (finding [3.3]) -------------------------------------- Inventaire pré-migration : 90 hex isolés en 42 codes uniques. - **Couleurs warm paper** : ``#fff`` → ``var(--paper, #fff)``, ``#16a34a`` → ``var(--fern, #16a34a)``, ``#dc2626`` → ``var(--clay-deep, #dc2626)``, etc. (~22 mappings sur les hex les plus fréquents). - **Greys neutres** : ``#f8fafc`` → ``var(--g-50, ...)``, ``#f1f5f9`` → ``var(--g-100, ...)``, ``#94a3b8`` → ``var(--g-300, ...)``, etc. - **Nouvelles palettes catégoriques** ajoutées à ``_design_tokens.css`` pour 18 hex qui ne mappaient pas aux tokens existants : - ``--accent-info`` (blue : header info OCR side-by-side). - ``--accent-ocr`` (cyan : chip étape OCR). - ``--accent-llm`` (violet : chip étape LLM, correction). - ``--accent-halluc`` (pink : tag hallucination). Chacun avec variantes ``-soft`` / ``-edge`` / ``-deep`` / ``-mid`` selon les usages observés. Ces accents sont **distincts** de la palette qualité ``--fern / --slate / --clay / --butter`` : ils servent à différencier des CATÉGORIES (étape, type de contenu) pas un niveau de qualité. Couverture du guard étendue à ``*.css`` ---------------------------------------- ``_discover_templates`` glob désormais aussi ``*.css``. ``_styles.css`` (~2600 LOC) et ``_design_tokens.css`` (~150 LOC) couverts par le guard. Preuve anti-theater ------------------- Sabotage ``color: var(--fern, #16a34a)`` → ``color: #16a34a`` ligne 1561 d'une vraie règle CSS active. Test échoue avec assertion exacte (``ligne 1561 : #16a34a``). Restauration → 61/61 verts (37 renderers + 17 templates HTML + 2 CSS + 5 top-level py). Note de risque visuel --------------------- Le test passe sur la conformité hex mais ne garantit PAS l'absence de régression visuelle. Les ``var(--xxx, #hex)`` utilisent le token CSS si supporté (browsers ≥ 2017), sinon fallback hex. Vérification visuelle recommandée via ``picarones demo --output /tmp/test.html``. Sous-phase restante Phase 18 : **18d** ``_app.js`` PALETTE + ``engineColor()`` ranking ordering — TRÈS RISQUÉ (intestable sans browser). ### Phase 19-23 — Refonte IA 4 onglets XerOCR + retrait Analyses (mai 2026) Le chantier S5-S12 (Track REPORT plus haut) avait posé l'IA 4 vues mais ~8 charts Chart.js et la matrice de corrélation interactive vivaient encore dans un 5e onglet temporaire « Analyses ». Phase 19-23 termine la migration, supprime l'onglet, et nettoie 225 LOC de dead code post-Étage 4. - **Phase 19** — Nav bar refondue : padding ``_styles.css`` symétrique ``6px 14px`` (était ``6px 10px 6px 18px``), 3 boutons d'action enveloppés dans ``