Picarones / CHANGELOG.md
Claude
chore: finir le retrait execution_mode (audit du commit precedent)
a23e336 unverified

Changelog — Picarones

Tous les changements notables de ce projet sont documentés dans ce fichier.

Le format suit Keep a Changelog. La numérotation de version suit Semantic Versioning.

Politique de versionning : voir 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).

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 <details> 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 <h1> regroupés en 1 seul (header) + 4 <h2> (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 <h3> 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 <details> 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 :

    • <img> 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 <span class="doc-card-difficulty doc-card-difficulty- qui ne peut venir QUE du rendu (la CSS écrit les classes séparées).
    • [2.2] test_pareto_toggle_availability_init_function_present vérifiait juste la présence du nom de fonction. Une fonction transformée en no-op (function _initParetoToggleAvailability() {}) aurait passé — le bug [1.1] de Phase 16 a justement transité par cette faiblesse. Test extrait désormais le corps de la fonction et exige 4 opérations comportementales : querySelectorAll('.pareto-toggle'), lecture pareto[axis], disabled et aria-disabled. Vérifié par sabotage : la fonction no-op fait échouer le test sur la première assertion.
    • [2.3] test_scatter_colors_follow_ranking ne paramétrait qu'un seul des 4 scatters Crosses (_build_cer_gini_scatter). Si quelqu'un oubliait engine_rank= dans un des 3 autres (cost, anchor, time), le test restait vert silencieusement — régression silencieuse de la cohérence badge possible. Désormais paramétré sur les 4 builders avec une fixture qui peuple les 4 sources (pareto.cost.points, gini_vs_cer, ratio_vs_anchor, engines[].mean_duration_seconds). Vérifié par sabotage : retrait de engine_rank= d'UN seul builder fait échouer UN seul des 4 cas paramétrés.
    • [2.4] _find_isolated_hex annonçait dans sa docstring « Ignore les lignes commençant par # » — comportement jamais implémenté. Doc alignée sur le code : la fonction strippe uniquement les commentaires inline avec espace avant ET après # ; les commentaires en début de ligne sans espace après le # sont une limitation acceptée (cas marginal absent des renderers actuels).
    • [3.6] MIGRATED_RENDERERS était une whitelist statique de 26 fichiers. Un nouveau renderers/new_chart.py avec fill="#abcdef" passait à travers — angle mort. Logique inversée : _EXEMPTED_FROM_HEX_CHECK (vide à date sauf __init__.py) + auto-discovery via RENDERERS_DIR.glob("*.py"). Tout fichier ajouté au dossier est automatiquement couvert. Méta-test test_discovery_covers_all_renderer_files garde contre un discovery qui retournerait une liste vide par erreur (qui ferait passer le test paramétré de façon vacuous). Vérifié par sabotage : création d'un fichier mock pollué dans le dossier → détecté immédiatement par le nouveau test paramétré, sans modification de la whitelist.
  • Phase 18a — hex → tokens dans les templates HTML :

    Premier morceau du chantier migration CSS (4 sous-phases prévues). Scope minimal pour valider le pattern avant d'attaquer les hex plus volumineux de _styles.css et _app.js.

    • [3.4] view_ranking.html contenait 4 hex inline (#16a34a, #ca8a04, #ea580c, #dc2626) pour les legend-dots du seuil CER. Migrés vers var(--fern, #16a34a), var(--butter-deep, #ca8a04), var(--clay, #ea580c), var(--clay-deep, #dc2626).
    • Bonus détecté par le guard étendu : base.html.j2 contenait 3 hex inline (#7f1d1d fond bandeau démo, #fff texte, #fca5a5 bordure) pour le bandeau d'avertissement « DONNÉES DE DÉMO ». Migrés vers var(--clay-deep, ...), var(--paper, ...), var(--clay-soft, ...).
    • Guard test étendu : nouvelle fonction _discover_templates
      • test_no_isolated_hex_in_template couvre désormais 17 templates HTML/Jinja2 (auto-discovery via glob, comme côté renderers). _EXEMPTED_TEMPLATES reste vide.

    Preuve anti-theater : sabotage d'un var(--fern, ...)#16a34a autonome dans view_ranking.html → test catch immédiatement la régression.

  • Phase 18b — hex → tokens dans CSS inliné Python (render.py + comparison.py) :

    Deuxième morceau du chantier migration CSS. Couvre les modules Python qui embarquent du CSS dans des string literals triple-quoted — render.py:_INLINE_CSS et comparison.py's <style> 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 : #fffvar(--paper, #fff), #16a34avar(--fern, #16a34a), #dc2626var(--clay-deep, #dc2626), etc. (~22 mappings sur les hex les plus fréquents).

    • Greys neutres : #f8fafcvar(--g-50, ...), #f1f5f9var(--g-100, ...), #94a3b8var(--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 <div class="nav-actions"> avec gap interne propre. 9 tests structurels + double sabotage anti-theater.
  • Phase 20 — PALETTE XerOCR + engineColor() par rang CER : _app.js perd la PALETTE Tailwind statique (10 hex vifs), gagne un _CHART_PALETTE (5 tokens) lu via getComputedStyle au boot ; _ENGINE_RANK_MAP construit depuis DATA.ranking (miroir JS de _engine_rank_map() Python). 11 callsites migrés (index → nom de moteur). 15 tests + 3 sabotages.
  • Phase 21A — Engines/Tableau : dispatch chart-duration + chart-bootstrap-ci + economics_view_html depuis view_analyses vers engines_table.html. Trigger buildCharts() étendu aux vues à charts. Nouvelle clé i18n h_economics (parité FR/EN).
  • Phase 21B — Engines/Diagnostics : dispatch chart-taxonomy + chart-reliability + error-clusters-container + 6 sections HTML legacy (readability_html, advanced_taxonomy_view_html, diagnostics_view_html, rare_token_recall_html, taxonomy_cooccurrence_html, taxonomy_intra_doc_html). 10 tests + 2 sabotages.
  • Phase 21C — Documents + Crosses : dispatch chart-cer-doc vers documents.html ; chart-quality-cer + pareto-chart (avec toolbar 3 axes + assumptions details) + marginal_cost_html vers crosses.html. 19 tests + 3 sabotages. Wrapper Pareto passe de chart-card pareto-card à card xer-chart-card pareto-card (convention XerOCR).
  • Phase 21D — Cleanup JS corr-matrix mort : suppression de initCorrelationMatrix, renderCorrelationMatrix, corrColor (dupliquaient le renderer Python build_correlation_matrix_html
    • 10 hex CSS-in-JS hors palette). Clé i18n orpheline corr_engine_label retirée (parité 635/635). 7 tests + 3 sabotages.
  • Phase 22+23 — Suppression onglet Analyses + view_results en <details> : view_analyses.html supprimé, bouton nav retiré, XER_VIEWS nettoyé. view_results_html (projections EvaluationView : TextView, AltoView) intégré en <details> repliable au bas de engines_diagnostics.html. Whitespace Jinja2 {%- if %} introduit pour éviter l'accumulation de newlines en conditionnels chaînés (régression du garde-fou cosmétique). 13 tests + 3 sabotages. Phases combinées en un seul commit pour éviter l'état intermédiaire où view_results_html serait computed mais non rendu.

5779 tests passants après Phase 22+23.

Audit X1-X4 — remédiation post-Phase 23 (mai 2026)

Audit adversarial multi-agents (5 angles indépendants) post-Phase 19-23 a remonté 4 catégories de findings, traités en 4 commits atomiques avec discipline test-first + sabotage anti-theater.

  • X1engineColor() self-healing contre race condition deeplink : bug runtime non détecté en Phase 20. _routing.js (chargé avant _app.js) enregistre son handler DOMContentLoaded qui s'exécute AVANT celui de _app.js. Sur deeplink #engines/#documents/#crosses, buildCharts() se déclenche → engineColor() lit _CHART_PALETTE encore vide → fallback fern hardcodé retourné pour TOUS les moteurs (charts monochromes verts uniformes). Fix : pattern self-healing, engineColor() appelle _ensureChartPaletteInitialized() et _ensureEngineRankMapInitialized() (idempotentes) en première instruction. Fallback hex retiré (l'invariant tient toujours via lazy init). 7 nouveaux tests TestEngineColorLazyInit + TestBootOrder remplacé par TestBootWarmup (2 tests — les anciens étaient devenus theater post-refactor, passaient en matchant la définition de fonction au top du fichier). 5 sabotages anti-theater validés.
  • X2 — Durcissement de 7 tests theater : 7 assertions modifiées (initialement or "X" in html.lower() qui matchaient trivialement — « wilcoxon » : 129 occurrences dans HTML, « correlation » : 21, « venn » : 16, « Gini » : 33, « Ancrage » : 17, VLM : multiple) remplacées par des ancres structurelles uniques (id="...", class="...", aria-label="..."). Découverte : un replace(..., ..., 1) faible peut faire passer un sabotage à tort si la chaîne apparaît plusieurs fois ; et les aria-label sont résolus via i18n au rendu, pas via le renderer Python. 6 sabotages anti-theater validés.
  • X3 — Consolidation margin-top: 1.25rem inline : 20 occurrences répétées dans 5 templates retirées au profit d'une règle CSS unique .xer-chart-card. Margin-collapse standard avec .card { margin-bottom: 1.25rem } garantit le spacing visuel identique (parents .view.active et .xer-subpane.active sont display: block, collapse applique — verifié). Cas spécial Venn : border-left:3px solid var(--butter) préservé. 7 tests + 3 sabotages. Découverte : test-theater dans mon propre test initial (regex matchait un commentaire CSS qui mentionnait la propriété) corrigé via strip /* */ et exigence du ; final.
  • X4 — Null check _switchView + retrait dead code Étage 4 : _switchView accédait getElementById(...).classList sans null check → crash sur hash invalide (#characters, #foo). Fix : garde + fallback silencieux vers overview via window.showXerView + console.debug pour traçabilité. Retrait de 6 fonctions JS dormantes (initCharView, renderCharView, renderConfusionHeatmap, renderLigatureDetail, renderTaxonomyDetail, showConfusionExamples), du let charViewBuilt, de characters:'caract' dans tabMap, et du if (name === 'characters'). ~225 LOC nettes retirées de _app.js. Bonus : 6 hex hardcodés inline dans renderConfusionHeatmap retirés (dette CSS-in-JS qui aurait échappé au guard hex). 4 fonctions de test ajoutées (dont 1 paramétrée à 7 instances → 10 cas exécutés au total) + 3 sabotages.

5801 tests passants post-X4. Ruff propre. i18n parité 635/635.

Apprentissages méta de l'audit :

  • Les sabotages doivent eux-mêmes être audités : un replace(..., 1) faible passe à tort, un test qui grep un commentaire CSS devient theater.
  • Les ancres de test doivent être structurelles (IDs uniques, classes spécifiques, combinaisons exactes), pas sémantiques (mots génériques comme « correlation » ou « wilcoxon » qui matchent du bruit dans le HTML).
  • Le pattern self-healing élimine les race conditions à la racine plutôt que de les masquer par un ordre d'exécution fragile.
  • Les claims de commit messages doivent être croisées contre le diff réel — drifts numériques mineurs des commits initiaux (X1 : 5 claim/7 réel ; X2 : 6 claim/7 réel ; X3 : 21 claim/20 réel ; X4 hex : 12 claim/6 réel) corrigés dans cette entrée CHANGELOG.

Méta-audit Z1-Z3 — anti-theater systémique des tests (mai 2026)

Audit adversarial post-Y4 (« consultant externe mandaté pour détruire ») a découvert que la discipline test-first + sabotage revendiquée tout au long de l'audit X-Y était elle-même partiellement theater. Mes sabotages remplaçaient ou supprimaient le code ; ils ne le commentaient JAMAIS, ni ne préservaient sa structure tout en cassant sa sémantique. Trois classes de theater systémique exposées :

  • Z1 — Theater par commentaire (systémique, 14+ tests affectés) : Les tests qui grep du source code (JS, CSS, HTML, Jinja2, Markdown) sans strip de commentaires sont trompés par un sabotage qui COMMENTE le code. La chaîne recherchée reste dans le fichier mais n'a aucun effet runtime. Mesuré : 14/14 tests palette/init passent en commentant le top de _app.js. Fix : tests/_strip_helpers.py (helper centralisé pour 5 langages)

    • refactor de 7 fichiers de tests. 17 tests méta valident le helper lui-même.
  • Z2 — Theater par sabotage qui préserve la structure : Un sabotage qui PRÉSERVE les chaînes recherchées tout en BRISANT la sémantique runtime (early-return après guard d'idempotence, constante hardcodée pour le rank, init dans un if(false)) passait à travers Z1. 3 sabotages distincts non détectés :

    • early-return réintroduisait silencieusement le bug X1
    • const rank = 0 faisait que engineColor retournait fern pour tous les moteurs
    • init dans if(false) rendait l'init unreachable

    Fix : 2 tests structurels plus stricts dans test_app_js_palette.py qui vérifient la SÉMANTIQUE post-guard (assignation effective à _CHART_PALETTE, lookup de rank utilise engineName).

  • Z3 — Theater par decoy dans le HTML rendu : les tests qui grep le HTML produit par picarones demo (fixtures html_s7, html_report) sont trompés par un decoy dans un commentaire HTML qui survit au rendu Jinja2 :

    <!-- decoy id="crosses-venn-title" --> <h3 id="DEAD-TITLE">

    Le grep matche le decoy, le test passe, mais l'utilisateur ne voit pas le titre. Fix : strip <!-- --> HTML avant le grep dans les 7 assertions X2 (Venn, Wilcoxon, error-clusters, correlation, Gini scatter, Anchor scatter, VLM badge).

Apprentissage méta :

  • Mes sabotages anti-theater étaient eux-mêmes incomplets — il faut SYSTÉMATIQUEMENT tester 4 modes de defeat : (1) suppression, (2) commentaire, (3) préservation structurelle + bris sémantique, (4) decoy dans le rendu.
  • Les tests structurels (grep sur source) ont des LIMITES fondamentales — seuls les tests runtime via JS engine (jsdom, Playwright) garantissent la sémantique exécutoire. Décision pragmatique 0.9.x : défense multi-couches structurel (Z1+Z2+Z3) documentée comme tel, runtime opt-in à évaluer pour 1.0.

Audit runtime indépendant (via Node + jsdom durant la session) :

  • engineColor() retourne des OKLCH valides pour les 5 engines du demo (vérifié par eval inside jsdom)
  • _switchView() robuste contre tous les edge cases testés : "", null, undefined, "foo", "<script>alert(1)</script>", "../etc/passwd" → tous redirigent vers overview sans crash
  • engineColor() robuste contre prototype pollution (__proto__, constructor, toString) via Object.prototype.hasOwnProperty.call
  • Les 6 fonctions retirées en X4 (initCharView etc.) sont effectivement undefined sur window post-load

5 sabotages adversariaux finaux (3 Z1 + 3 Z2 + 2 Z3) tous détectés.


[Unreleased] — Migration Option B vers RunOrchestrator (mai 2026)

Branche claude/test-alto-pipelines-qyFsL — chantier de migration complète vers RunOrchestrator comme entry-point canonique pour lancer un benchmark, avec livrable métier ALTO documentaire. 20+ commits sur ~13 jours d'effort, 4830 → 4897 tests passants (+67).

Nouveautés — valeur métier

  • picarones.RunOrchestrator exposé au niveau racine. Consomme un RunSpec Pydantic validé et expose 4 fichiers JSONL natifs (run_manifest.json, pipeline_results.jsonl, artifacts_index.jsonl, view_results.jsonl) en plus du BenchmarkResult legacy (via spec.output_json).
  • RunSpec étendu avec 7 nouveaux champs : char_exclude, normalization_profile, partial_dir, entity_extractor, profile, output_json, timeout_seconds_per_doc. Tous validés par Pydantic (_validate_profile_is_known, _validate_entity_extractor_format).
  • 3 vues canoniques natives dans le RunResult : text_final, alto_documentary, searchability. Chacune produit ses propres ViewResult typés avec metric_values, failed_metrics, projection_report, warnings.
  • TesseractAdapter expose ALTO natif via le flag expose_alto (off par défaut, compat ascendante). Premier adapter du repo à produire un Artifact ALTO_XML. Valide structurellement la sortie avant promotion (résistance XML mal formé).
  • AltoView étendu : 7 métriques par défaut au lieu de 3. Les 4 nouvelles (alto_text_cer/wer/mer/wil) opèrent sur le texte plat extrait de l'ALTO via extract_text_from_alto — permettent de détecter une régression textuelle même quand la structure est préservée.
  • Rapport HTML multi-vues : nouvelle section view-results-section rendue par picarones/reports/html/renderers/view_results.py. Affiche un tableau Métrique × engine par vue avec moyennes, et liste explicitement les pipelines OMIS de chaque vue (critique pour AltoView : un OCR sans ALTO ne doit pas être omis silencieusement). Adaptive : section absente si benchmark.view_results vide (chemin legacy intact).
  • BenchmarkResult.view_results : nouveau champ optionnel {view: {engine: {doc: {metric: value}}}} peuplé par le converter depuis le RunResult.document_results[*].view_results. Consommé par le rapport HTML et accessible aux clients pour analyses ad-hoc.

BREAKING — Retrait du legacy (Phase B3-final, migration nette)

Suppression complète de l'entry point legacy et de ses modules helpers internes. Les call sites CLI/Web/tests ont été migrés vers le pattern 3 étapes explicite (Option 10 du chantier) :

# Pattern moderne — 3 étapes visibles, pas de shim caché
args = prepare_preset_args(corpus, engines, workspace_dir=...)
orch_result = RunOrchestrator(out).execute_preset(**dataclass_fields)
result = run_result_to_benchmark_result(orch_result.run_result, ...)
  • run_benchmark_via_service supprimée — utilisez RunOrchestrator + prepare_preset_args (Python) ou RunOrchestrator.execute(RunSpec) (YAML).
  • Modules supprimés en Phase B3-final (~1700 LOC nettes pour cette phase isolée ; la branche complète cumule aussi l'audit code-quality qui ajoute massivement — voir entrée suivante) :
    • benchmark_runner (entry point legacy)
    • _benchmark_execution (helper interne orchestration)
    • _benchmark_orchestration (run_benchmark_unified / run_benchmark_with_partial)
    • legacy_runner_compat (shim intermédiaire B3 introduit puis supprimé dans le même chantier — voir entrée Option 10)
  • Tests d'invariance supprimés — leur rôle (garde-fou pendant la migration) est rempli, la migration est terminée :
    • tests/integration/test_migration_invariance.py
    • tests/integration/snapshots/migration_invariance.json

Modifié — Audit B3-final correctif (mai 2026)

L'audit implacable de la branche post-migration a identifié 4 demi-chantiers critiques : les features livrées en B5/B6/B2 étaient implémentées en interne mais inaccessibles aux utilisateurs CLI/Web. Corrections appliquées :

  • CLI : 5 nouvelles options ajoutées à picarones run :
    • --views VIEW1,VIEW2,… (B6 multi-vues)
    • --expose-alto (B5 Tesseract ALTO XML)
    • --char-exclude CHARS (B2.5)
    • --partial-dir PATH (B2.3 resume)
    • --entity-extractor DOTTED_PATH (B2.4 NER) _engine_from_name propage expose_alto à Tesseract.
  • Web : BenchmarkRunRequest étendu avec views: list[ViewName], profile, partial_dir, entity_extractor, output_json. PipelineConfig.expose_alto activable par concurrent. Le worker run_benchmark_thread_v2 propage les nouveaux champs au pattern 3 étapes.
  • Helper test : tests/_migration_helpers.run_via_orchestrator reçoit un kwarg views (corrige la divergence test↔prod identifiée par l'audit — aucun test B4 ne couvrait précédemment le multi-vues via le helper).

Impact utilisateur :

picarones run -c ./corpus -e tesseract --expose-alto \
    --views text_final,alto_documentary,searchability \
    --profile standard

Génère désormais un rapport HTML avec 3 sections (TextView, AltoView, SearchView) + ALTO XML natif de Tesseract. La valeur métier C3 est enfin accessible aux utilisateurs.

Modifié

  • picarones.interfaces.cli._workflows : 6 commandes (run, diagnose, economics, edition, compare, robustness) utilisent désormais un helper local _run_orchestrator_for_cli qui mutualise le pattern 3 étapes. Comportement utilisateur identique.
  • picarones.interfaces.web.benchmark_utils.run_benchmark_thread_v2 : pattern 3 étapes inline. L'API REST POST /api/benchmark/run est inchangée pour les clients.
  • picarones.app.services.__init__ expose désormais PresetArgs, prepare_preset_args, run_result_to_benchmark_result comme API publique.
  • TesseractAdapter.output_types : étendu de {RAW_TEXT, CONFIDENCES} à {RAW_TEXT, CONFIDENCES, ALTO_XML} (set maximal). Les pipelines existants restent inchangés tant que expose_alto n'est pas activé.
  • build_text_view / build_search_view : nouveau kwarg char_exclude propagé jusqu'au DefaultEvaluationViewExecutor qui filtre les caractères avant calcul des métriques.

Migration utilisateur

Cf. docs/archive/2026-migration/option-b-user-guide.md pour le mapping complet des paramètres legacy → RunSpec, 4 cas concrets (corpus mémoire, partial_dir resume, NER attach, cancellation), et calendrier de retrait phasé.

Phases du chantier (référence interne)

  • B0 préparation (snapshot d'invariance + squelette feature parity + inventaire tests)
  • B1 RunSpec étendu (7 nouveaux champs + validators)
  • B2 porting des 7 features dans RunOrchestrator (progress_callback, cancel_event, partial_dir, entity_extractor, char_exclude + normalization_profile, profile hooks, output_json) → Checkpoint C1
  • B3 exports publics + DeprecationWarning + migration concrète des call sites CLI/Web via legacy_runner_compatCheckpoint C2
  • B4 migration des 6 fichiers de tests catégorie A (71 appels)
  • B5 TesseractAdapter.expose_alto (premier adapter ALTO natif)
  • B6 rapport HTML multi-vues + extension DEFAULT_ALTO_METRICSCheckpoint C3 (valeur métier livrée)
  • B7 deprecation finale (bannières + CHANGELOG initial)
  • B3-final (Option 10) — migration nette : helper prepare_preset_args + pattern 3 étapes inline dans CLI/Web/tests, puis suppression complète du shim legacy_runner_compat et des 3 modules purement legacy. -1700 LOC nettes.

[Unreleased] — Audit code-quality (mai 2026)

Branche claude/code-quality-audit-EeY0r — audit implacable du repo suite à la migration v2.0, suivi de 12 sprints correctifs (Phases 0 à 12 du plan d'audit).

BREAKING — ruptures API v2.0

Toutes ces ruptures sont immédiates, sans calendrier de dépréciation (règle de l'audit : « soit on supprime, soit on garde et on développe »). Migration directe documentée ci-dessous.

  • POST /api/benchmark/start retiré au profit de POST /api/benchmark/run. Le modèle Pydantic BenchmarkRequest (liste de moteurs plats) est remplacé par BenchmarkRunRequest (liste de PipelineConfig). Helpers associés supprimés : _legacy_request_to_run_request, run_benchmark_thread (v1 ; run_benchmark_thread_v2 reste). Migration :

    # avant (v1.x)
    POST /api/benchmark/start
    {"corpus_path": "...", "engines": ["tesseract", "pero_ocr"]}
    
    # après (v2.0)
    POST /api/benchmark/run
    {
        "corpus_path": "...",
        "competitors": [
            {"name": "tesseract", "engine_name": "tesseract"},
            {"name": "pero_ocr", "engine_name": "pero_ocr"},
        ],
    }
    
  • run_benchmark_via_service(..., max_workers=4) retiré — paramètre absorbé sans effet via # noqa: ARG001. Le rewrite passe par CorpusRunner.max_in_flight directement.

  • expand_legacy_keys() retiré de picarones.domain.artifacts — 0 caller en production. Le dict LEGACY_VALUE_ALIASES reste vivant pour le canonicalisation des manifests legacy.

  • JSON BenchmarkResult pré-v2.0 plus relisibles — le retrait de expand_legacy_keys interdit le round-trip depuis des sorties v1.x. Régénérer les benchmarks de référence.

  • Paramètre text_hint retiré de picarones.evaluation.synthetic._make_placeholder_png (était jamais lu).

Added — features inachevées débloquées

  • Agrégation sur-normalisation LLM corpus-wide : aggregate_over_normalization câblée via @register_corpus_aggregator(name="over_normalization", ...) (profils philological, diagnostics, full). Le hook extrait depuis DocumentResult.pipeline_metadata["over_normalization"] et alimente EngineReport.aggregated_over_normalization. Round-trip JSON préservé.

  • Journal de fallbacks importer end-to-end : la chaîne record_fallback → consume_fallback_log → BenchmarkResult.metadata → build_report_data → narrative.detect_importer_fallback est désormais branchée. Un fallback HTR-United mode démo apparaît dans la synthèse narrative avec traçabilité (URL, raison).

  • Profils de normalisation YAML versionnables :

    • CLI : picarones run --normalization-profile <ID-OR-PATH> — accepte un identifiant builtin ou un fichier .yaml versionné dans git.
    • API : POST /api/normalization/profiles/preview — valide un YAML utilisateur et retourne le profil sérialisé (preview, pas de persistance). Limite 64 KiB côté Pydantic.
  • register_default_metrics() exposée publiquement — remplace le side-effect import picarones.evaluation.metrics opaque en tête de picarones/__init__.py. Idempotente (sys.modules cache). Auto-déclenchement préservé pour rétrocompat.

  • RunResult accessible depuis picarones.pipeline.run_result — déplacé de app.results (compat shim conservé) vers la couche 4 pour respecter l'orientation des couches (reports/ ne peut plus importer depuis app/).

Changed — architecture & lisibilité

  • 8 __init__.py de couche : renumérotation Cercle N (incohérent, max 5, doublons) → Couche N (1 à 8, ordre du manifeste).
  • benchmark_runner.py : 1 700 → 1 584 LOC. Extractions :
    • _benchmark_ner.py (NER aggregation, ~100 LOC).
    • _benchmark_persistence.py (sérialisation JSON, ~15 LOC). Budget test_file_budgets resserré de 1 750 à 1 620.
  • robustness.py : 850 → 578 LOC. Suppression des 5 helpers pure-Python _apply_* et du stub _degrade_pure_python (Pillow est dep obligatoire, fallback sans valeur).
  • PipelineMode unifié : source unique picarones.domain.pipeline_spec.PipelineMode. Les 3 alias historiques (OCRLLMMode, OCRLLMPipelineMode, PipelineMode) deviennent des re-exports.
  • Validation chemin web factorisée : helpers validated_user_path / validated_user_output_dir dans interfaces/web/_path_helpers.py ; 2 routers migrés.
  • eScriptorium anti-SSRF : _get, _post et le téléchargement d'image utilisent désormais validate_http_url et download_url (cohérence avec IIIF/Gallica/HTR-United).
  • Tesseract lang validation : regex ^[a-zA-Z]{3,}(\+[a-zA-Z]{3,})*$ rejette les injections CLI (fra --user-words /etc/passwd).
  • CI : sync compteurs bloquant — nouveau job sync-counters exécute scripts/gen_readme_tables.py --check.
  • CI : 7 nouveaux tests d'architecture verrouillent les invariants pour bloquer la régression :
    • test_no_zombie_skips.py (interdit pytest.skip sur dep obligatoire).
    • test_no_legacy_imports_in_rewrite.py refondu — test actif contre la résurrection des paquets legacy supprimés (au lieu d'un LEGACY_PACKAGES = () vacuement vrai).
    • test_no_broad_pytest_raises.py — refuse les pytest.raises(Exception) (ratchet, baseline 24).
    • test_logger_prefix.py — refuse les logs sans préfixe [<module>] (ratchet, baseline 46).
    • test_live_test_markers.py — chaque fonction dans tests/integration/live/ porte @pytest.mark.live.
    • test_reports_layer_strict.py — interdit les imports reports/ → {adapters, app, interfaces}.
    • test_pipeline_mode_single_source.py — refuse toute nouvelle redéfinition de Literal["text_only", "text_and_image", "zero_shot"].
    • test_api_stable_modules_exist.py — chaque module cité dans api-stable.md doit s'importer.

Removed

  • POST /api/benchmark/start + helpers (cf. BREAKING).
  • BenchmarkRequest modèle Pydantic v1 (cf. BREAKING).
  • max_workers, text_hint paramètres morts.
  • expand_legacy_keys() (cf. BREAKING).
  • 5 helpers _apply_* + _degrade_pure_python dans robustness.py (~300 LOC).
  • 7 pytest.skip("click non installé") zombies dans tests/integration/test_chantier{4,5}.py (click est dep obligatoire — skip vacuement vrai).
  • 4 modules fantômes retirés de docs/reference/api-stable.md (pipeline.legacy_runner, pipeline.legacy_pipeline_benchmark, pipeline.legacy_pipeline_comparison, evaluation.metrics.pipeline_spec_loader).

Fixed

  • app.py : entry point HuggingFace cassé (picarones.web.app:apppicarones.interfaces.web.app:app).
  • 8 except: pass silencieux : tous remplacés par logger.warning("[<module>] ...") ou logger.debug(...) selon criticité (friedman_nemenyi, image_quality, iiif, clustering, path_security, benchmark_runner, job_store, robustness).
  • 10 pytest.raises(Exception) trop larges : précisés en FrozenInstanceError ou pydantic.ValidationError.
  • README.md : retrait du paragraphe « Legacy paths still present as shims » (faux depuis v2.0) ; mypy picarones/core/mypy picarones/domain/.
  • CLAUDE.md : compteur tests synchronisé (4 700 → réel), auto-contradiction « 12 vs 9 skipped » résolue, 18 → 20 détecteurs, 22 → 28 renderers.
  • Faux positifs bandit B608 documentés avec # nosec et commentaire de justification (sites SQL où les fields interpolés sont des littéraux internes, valeurs via ?).
  • eScriptorium : urlretrieve sans validation → download_url avec anti-SSRF.

Security

  • SSRF résiduel eScriptorium fermé (_get, _post, téléchargement images).
  • Injection CLI Tesseract bloquée (regex sur lang).
  • Aucune CVE introduite (pip-audit vert).

Stats

  • +158 nouveaux tests (4 686 → 4 784 passing). Ruff propre, bandit propre (1 LOW résiduel inoffensif), 0 régression.
  • 12 nouveaux modules créés (helpers extraits + tests d'invariant).
  • ~600 LOC mortes supprimées.

[Unreleased] — Chantier post-rewrite (mai 2026)

Branche claude/fix-module-rewiring-MHssX — réconciliation des chemins UI / API / runner / JSON / rapport après audit révélant des options ignorées, moteurs annoncés sans backend, surfaces filesystem ouvertes et round-trip JSON appauvri.

Added

  • Adapters KrakenAdapter et CalamariAdapter (couche 5) avec lazy imports. Auparavant annoncés par /api/engines mais sans factory branchée — benchmark web échouait silencieusement.
  • Extras pyproject [calamari] (calamari-ocr>=2.0.0) ; l'extra [kraken] préexistant pointe désormais sur un adapter réel.
  • BenchmarkResult.from_dict / from_json_object restaurent fidèlement toutes les analyses avancées (taxonomy, confusion_matrix, NER, calibration, philological, searchability, hallucination, numerical_sequence, readability) ; le rapport régénéré depuis JSON est désormais indistinguable du in-memory.
  • partial_store.compute_run_fingerprint + partial_path_for_engine : hash SHA-256 stable (engine_config + normalization_profile + char_exclude + corpus mtime/size + code_version) suffixé au nom du fichier partiel. Deux runs avec configs différentes ne se contaminent plus.
  • Workflows CLI diagnose/economics/edition génèrent désormais le rapport HTML automatiquement à côté du JSON (--no-html pour skipper). Les docstrings vendaient déjà les vues HTML correspondantes.
  • UI : boutons "💾 Sauvegarder config" / "📂 Charger config" dans l'écran benchmark (binding sur /api/config/save + /api/config/load qui existaient mais n'étaient appelés par personne).
  • UI : bandeau "Mode démo" sous le titre HTR-United quand le catalogue distant est inaccessible (champ is_demo exposé par le router).
  • Tests : tests/security/test_phase1_post_rewrite_wiring.py (~50 tests couvrant les 5 phases) + extensions de tests existants.

Changed

  • HTRUnitedCatalogue : router utilise from_remote(timeout=5) avec fallback automatique sur démo (au lieu de from_demo() exclusif). Variable d'env PICARONES_HTR_UNITED_OFFLINE=1 pour forcer démo (CI / déploiements offline).
  • upload_purge_task (RGPD) : démarrée par le lifespan FastAPI (auparavant définie mais jamais lancée). JobStore.create_job reçoit désormais un payload {"corpus": req.corpus_path} pour que la purge identifie les corpus actifs.
  • /api/benchmark/start : worker unifié — délègue à run_benchmark_thread_v2 après conversion BenchmarkRequest → BenchmarkRunRequest. Marqué deprecated dans les logs ; un seul chemin à patcher.
  • CLI engines : source de vérité unique avec adapters/ocr/factory._SUPPORTED. Plus de hardcode local [tesseract, pero_ocr] qui divergeait du web.
  • ReportGenerator.from_json : délègue à BenchmarkResult.from_json_object (simplification + fidélité).

Breaking Changes

  • CompetitorConfigPipelineConfig (renommage de classe).
  • PipelineConfig.ocr_engineengine_name : le field accepte aussi corpus (OCR pré-calculé) et des VLMs en zero-shot — le préfixe ocr_ était trompeur. Les clients qui envoyaient {"ocr_engine": "…"} doivent migrer vers {"engine_name": "…"} (Pydantic v2 ignore silencieusement l'extra → benchmark refuse).
  • PipelineConfig.pipeline_mode typé Literal["text_only", "text_and_image", "zero_shot"]. Toute autre valeur (y compris les anciens alias post_correction_text / post_correction_image) est rejetée en 422 par Pydantic — fini le fallback silencieux vers text_only qui masquait les configs invalides.

Security

  • output_dir validé (validated_path contre compute_workspace_roots) dans api_htr_united_import et api_huggingface_import — plus de path traversal écriture.
  • db_path validé dans /api/history/regressions — plus de lecture SQLite arbitraire. Pour pointer une base hors workspace, exporter PICARONES_HISTORY_DB.
  • ZIP collision de basename : a/img.png + b/img.png ne s'écrasent plus silencieusement — second renommé avec préfixe slug du dirname source.
  • ZIP image extraite validée : validate_image_safe (Pillow.verify, anti-bombe, limite taille) appelé sur chaque image lors de l'extraction — auparavant les images extraites passaient sans vérif (zip bomb jusqu'à 500 Mo brut).

Fixed

  • Round-trip BenchmarkResult.to_jsonfrom_json préserve désormais l'intégralité des analyses (reproductibilité scientifique).
  • Partial store fingerprint évite la réutilisation illégale de résultats entre runs avec configs différentes.

[0.9.0] — Legacy retirement complete (mai 2026)

Cette entrée porte le numéro de version dénommé « 2.0.0 » jusqu'au 2026-05-23, repositionné en 0.9.0 dans le cadre de l'alignement SemVer pré-1.0 (voir « Note de repositionnement » en tête de fichier). Le contenu n'a pas été réécrit : la prose mentionne « v2.0 » et « 1.x » telles qu'elles étaient employées à l'époque.

Breaking changes majeurs : suppression complète des paquets legacy. L'architecture canonique 8 couches (domain → formats → evaluation → pipeline → adapters → app → reports → interfaces) est la seule arborescence du code. Aucun shim, aucun _legacy/, aucun legacy_* subdir.

Suppressions (toutes Sprints A-H, mai 2026)

Top-level :

  • picarones/core/ — Lots A-G : domain, formats, evaluation
  • picarones/measurements/ — Lot D + E : evaluation/metrics, statistics
  • picarones/engines/, picarones/modules/ — Lot E : adapters/legacy_*
  • picarones/report/ — Lot F : reports/html (Sprint H.3 : reports_v2/reports/)
  • picarones/llm/, picarones/pipelines/, picarones/cli/, picarones/web/, picarones/extras/, picarones/fixtures.py — Sprints F, G, H.1

Adapters legacy :

  • picarones/adapters/legacy_engines/ (Sprint H.2.d) : BaseOCREngine, EngineResult, LegacyOCREngineExecutor, engine_from_name, et les 5 adapters Tesseract/Pero/Mistral OCR/Google Vision/Azure DI legacy. Remplacés par picarones.adapters.ocr.* (BaseOCRAdapter natif, factory ocr_adapter_from_name).
  • picarones/adapters/legacy_pipelines/ (Sprint H.2.c) : OCRLLMPipeline, PipelineMode. Remplacés par picarones.pipeline.llm_pipeline_config.OCRLLMPipelineConfig
    • picarones.pipeline.llm_pipeline_builder.make_ocr_llm_pipeline_spec.
  • picarones/adapters/legacy_modules/ (Sprint H.2.a) : TextToAltoMonoRegion.

Interfaces legacy :

  • picarones/interfaces/cli/_legacy/ + stubs canoniques inachevés interfaces/cli/{run,report,import_corpus}.py (Sprint H.4) : consolidé en interfaces/cli/ (16+ commandes Click).
  • picarones/interfaces/web/_legacy/ + stubs canoniques inachevés interfaces/web/{__init__,app,security}.py + routers/, templates/, static/, i18n/ (Sprint H.4) : consolidé en interfaces/web/ (FastAPI + UI Jinja2 + SSE benchmark + ZIP upload).

Renames

  • picarones/reports_v2/picarones/reports/ (Sprint H.3).
  • picarones/app/services/_legacy_runner_adapter.pypicarones/app/services/benchmark_runner.py (Sprint H.4) : drop le préfixe _legacy_ ; c'est l'entry point public des interfaces vers BenchmarkService.
  • picarones/app/services/_legacy_partial_store.pypicarones/app/services/partial_store.py (Sprint H.4).

Features ajoutées (Sprints D)

  • partial_dir : reprise sur interruption (NDJSON per-engine, Sprint D.2.b) — un benchmark crashé peut reprendre sans perdre le travail déjà fait.
  • entity_extractor : NER attach post-bench (Sprint D.2.e) — metrics NER calculées + agrégées sur les documents avec GT ENTITIES.
  • over_normalization : détection automatique pour les pipelines OCR+LLM avec OCR amont (Sprint D.2.d).
  • validate_profile() au démarrage du benchmark (Sprint D.2.f) : un profil inconnu lève ValueError avant tout calcul.

Architecture

  • LEGACY_PACKAGES = () dans tests/architecture/test_no_legacy_imports_in_rewrite.py : plus aucun paquet legacy.
  • test_legacy_canonical_parity.py supprimé (Sprint H.5) — la table de parité est sans objet à v2.0.
  • test_layer_imports_are_legal[layer-X] passe pour toutes les couches.

Migration depuis 1.x

# AVANT (1.x)
from picarones.cli import cli
from picarones.engines.tesseract import TesseractEngine
from picarones.measurements.runner import run_benchmark
from picarones.pipelines import OCRLLMPipeline, PipelineMode
from picarones.report.generator import ReportGenerator

# APRÈS (2.0)
from picarones.interfaces.cli import cli
from picarones.adapters.ocr.tesseract import TesseractAdapter
from picarones.app.services.benchmark_runner import run_benchmark_via_service
from picarones.pipeline.llm_pipeline_config import OCRLLMPipelineConfig
from picarones.reports.html.generator import ReportGenerator

Statistiques

  • 4126 tests passing, 0 failed.
  • ~10 paquets legacy supprimés.
  • ~50000 LOC de legacy retirées.
  • Architecture 8 couches respectée (vérifiée par 4 tests architecturaux : layer_imports, no_legacy_imports, layer_dependencies, file_budgets).


Historique pré-v2.0 : les versions antérieures à 2.0 (janvier 2025 → avril 2026) sont archivées dans docs/archive/changelog-pre-v2.md.