Spaces:
Running
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 :
- Surface UI complète — exposition de tous les champs du modèle
BenchmarkRunRequest, parité fonctionnelle avec la CLI (compare,robustness,history). - Parité importeurs corpus — IIIF, Gallica, eScriptorium accessibles depuis l'UI web.
- 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 :
[0.9.0] — Legacy retirement complete (mai 2026)— aboutissement.[Unreleased] — Migration Option B— sous-chantier intégré.[Unreleased] — Audit code-quality— sous-chantier intégré.[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_modeest retiré de tous les adapters (BaseOCRAdapter,BaseLLMAdapter,BaseModule) ainsi que le typeExecutionMode(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 duCorpusRunnermais le runner est thread-only par conception et n'a jamais lu ce champ. - Le type
ExecutionModen'est plus ré-exporté parpicarones.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.mdetdocs/reference/api-stable.mdréconciliés avec l'arborescence canonique 0.9.0.README.md,CLAUDE.md,docs/developer/module-policy.mdmis à 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 (commits047ab1betcd67184) 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.0officialisé (auparavant la sortie « 2.0 » du rewrite a été renumérotée — cf. « Note de repositionnement » ci-dessus).picarones/domain/_version_fallback.pyintroduit comme unique source de la version (FALLBACK_VERSION = "0.9.0") — garde-fou partests/architecture/test_single_version_source.pyettest_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
BenchmarkRunRequestdans 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.csspartagé avec l'app (palette warm paper + halftone Xerox Star + IBM Plex + Bricolage Grotesque + accents oklchfern/slate/clay/ butter), guard-railtest_xerocr_tokens.pyqui vérifie la parité tokens app↔rapport (42 assertions). - S5b — Squelette nouvelle IA 4 vues + routeur hash :
_routing.js(vues XerOCR + sous-ongletsengines/{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 neutrelegacy-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-i18nmanquantes ajoutées (tab_*,overview_*,engines_*,engines_sub_*) ; 4 eyebrows + 5 aria-labels hardcodés FR migrés versdata-i18n-attr-aria-label; 4<h1>regroupés en 1 seul (header) + 4<h2>(vues) ;engines-hero-statspeuplé (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…) ;_switchViewne casse plus la nav XerOCR au drill-in ; Overview ↔ Documents lisent désormais la même source pour les strates ; hero scatter counter aligné surisinstance; modèles Anthropic fallback corrigés (modèles inventés retirés) ; triple duplication_ENGINE_ACCENTSfactorisée danspicarones/reports/_helpers/engine_badges.py;esc()legacy échappe désormais l'apostrophe (XSS viaonclick='...') ; NORM par défaut affiche—au lieu de"nfc"fabriqué. - Phase 2 — sécurité S2/S3 (C1 + M3) : nouveau helper
_import_guards.pyqui appliquestate.enforce_rate_limitsystématiquement suriiif/gallica/escriptorium; en mode public, plafonnemax_resolutionà 2048 px et refusepages="all"; nouveau_RevalidatingRedirectHandlerdans_http.pyqui re-valide chaque redirect HTTP (anti-SSRF post-redirect, parade AWS metadata). - Phase 3 — polish + tests :
BenchmarkHistorypasse en WAL mode + retry 30s (concurrence robuste) ;_compare.jsajoute MAX_BYTES 50 Mo +Number.isFinite(anti-NaN) +label_otherdead code retiré ; renderersdata-valueéchappés ;name=None→"—"au lieu de"None";worst_linesutilise<h3>sémantique ;oklch()reçoit fallbacks#hex(Chrome <111 / Firefox <113 / Safari <15.4) ;test_renderer_exception_logged_not_raisedteste effectivement le log viacaplog; fichier sprint-nommé renommé ;tests/reports/conftest.pycentralise le fixturedemo_htmlscope session.
DoD :
- 5560+ tests passent, 0 failed.
ruff checkpropre.- 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é deEngineReportversengines_summarypuis 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_labellisaitsnapshots.normalization_profilequi n'est jamais produit ; clé canoniquesnapshots.normalizationrétablie — le snapshot autoritatif n'était plus silencieusement ignoré avec fallback systématique surmeta.metadata.
Phase 2 — Vue Stabilité fonctionnelle depuis SQLite history :
- Nouveau module
picarones/reports/html/data/history.py(306 LOC) qui pont la SQLiteBenchmarkHistoryversreport_data— alimentelongitudinal+baseline_difficulty+baseline_comparisons(consommé par le détecteur narratifengine_off_baseline) +longitudinal_trends(alias consommé parregression_in_history). ReportGeneratoracceptehistory=ouhistory_db_path=(défaut env varPICARONES_HISTORY_DBpuis~/.picarones/history.db).- Empty state composite Stabilité refactoré : retourne
""quand les 4 sources internes sont vides, exposé séparément viabuild_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.
- Nouveau module
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. Nouveaucompute_taxonomy_comparison_sectiondansdata/extra_metrics.py. - Hero stat Crosses comptait les scatters avec critère
>= 2points mais_build_cer_cost_scatterrendait dès>= 1: hero disait « 0 scatters » mais un scatter dégénéré avec 1 point s'affichait quand même. Alignement strictisinstance + >= 2partout.
- Le composite Crosses lisait
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é parannotate_documents_with_difficultymais 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 (helpersvg_open+ calibration + pipeline_dag + crosses scatters) — sans cet attribut,height:autosur écran étroit écrasait verticalement les charts.
- Badge difficulté intrinsèque (Facile / Modéré / Difficile /
Très difficile, seuils 0.25 / 0.50 / 0.75 sur
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 debuildCharts()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.
- 6 fonctions Chart.js (
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.onkeydownajouté pour Enter- Espace avec
preventDefault.
- Espace avec
- Fallback
"(par défaut)"du label normalization devient i18n-aware vialabels["overview_normalization_default"].
- Lignes ranking Overview avaient
Phase 7 — Couleurs scatters cohérentes + a11y Pareto + responsive :
_build_scatter_svgattribuait la couleur des cercles viaenumerate(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 depuisreport_data["ranking"]jusqu'à l'attribution de couleur — cohérence inter-tabs verrouillée.- Toggles Pareto (cost / speed / co2) gagnent
aria-pressedrole="group"sur le toolbar — WCAG 2.1 AA fix (4.1.2 Name, Role, Value).setParetoAxissynchronisearia-presseden 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 dansDATA.pareto— empêche un clic qui ouvrirait un canvas vide « Données insuffisantes ».
- Pipelines OCR+LLM rendaient
Phase 9 — Image CLS + Escape compare + corpus mismatch :
<img>Documents gallery sanswidth/height→ CLS au premier paint ; sansonerror→ 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
_wireBannerDismisscâble click + Escape avec auto-désinscription via flag_disposed. _detectCorpusMismatchémet un préfixe ⚠ + chip.xer-compare-warningquand 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.pyrécrite avec contrat explicite «idx= rang dansreport_data["ranking"](CER ascendant) » + warnings anti-patterns (ne PAS passerenumerate(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 checkpropre 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) hardcodaitif (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 handlerskeydownsurdocumentà chaque comparaison successive (le flag_disposedne désinscrivait l'ancien handler qu'au prochain keydown — jamais si l'utilisateur ne tapait plus). Singleton module-level_currentKeyHandlerqui 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.dbjusqu'au GC + verrouillait le fichier sous Windows. Refactor en(history, owned)tuple —build_history_sectionsferme la connexion enfinallyUNIQUEMENT 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.75dupliqués dans 3 fichiers (evaluation/metrics/difficulty.py,renderers/difficulty.py,renderers/documents_gallery.py). Nouveaudifficulty_bucket(score) → strdans le module évaluation = source unique de vérité.difficulty_label,difficulty_coloret_difficulty_badge_htmlconsomment 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) retournaitNonequand la clé existait avec valeurNone, ce qui affichait"None"littéral dans le ranking card.engines_table.pyavait déjà documenté le piège ; pattern alignérow.get("engine") or "—"partout.
- [1.1]
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
_currentKeyHandlerintroduisait une fenêtre théorique où un keydown qui arrivait entreremoveEventListener(old)et la réassignation pouvait lire la mauvaise valeur. Handler stocké désormais directement sur l'élément DOMbanner._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_bucketcomme 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ésormaisdifficulty_slugannoté 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.
- [1.2] race condition résiduelle : le singleton module-level
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_cardscherchait"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_presentvé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'), lecturepareto[axis],disabledetaria-disabled. Vérifié par sabotage : la fonction no-op fait échouer le test sur la première assertion. - [2.3]
test_scatter_colors_follow_rankingne paramétrait qu'un seul des 4 scatters Crosses (_build_cer_gini_scatter). Si quelqu'un oubliaitengine_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 deengine_rank=d'UN seul builder fait échouer UN seul des 4 cas paramétrés. - [2.4]
_find_isolated_hexannonç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 nouveaurenderers/new_chart.pyavecfill="#abcdef"passait à travers — angle mort. Logique inversée :_EXEMPTED_FROM_HEX_CHECK(vide à date sauf__init__.py) + auto-discovery viaRENDERERS_DIR.glob("*.py"). Tout fichier ajouté au dossier est automatiquement couvert. Méta-testtest_discovery_covers_all_renderer_filesgarde 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.
- [2.1]
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.csset_app.js.- [3.4]
view_ranking.htmlcontenait 4 hex inline (#16a34a,#ca8a04,#ea580c,#dc2626) pour les legend-dots du seuil CER. Migrés versvar(--fern, #16a34a),var(--butter-deep, #ca8a04),var(--clay, #ea580c),var(--clay-deep, #dc2626). - Bonus détecté par le guard étendu :
base.html.j2contenait 3 hex inline (#7f1d1dfond bandeau démo,#ffftexte,#fca5a5bordure) pour le bandeau d'avertissement « DONNÉES DE DÉMO ». Migrés versvar(--clay-deep, ...),var(--paper, ...),var(--clay-soft, ...). - Guard test étendu : nouvelle fonction
_discover_templatestest_no_isolated_hex_in_templatecouvre désormais 17 templates HTML/Jinja2 (auto-discovery via glob, comme côté renderers)._EXEMPTED_TEMPLATESreste vide.
Preuve anti-theater : sabotage d'un
var(--fern, ...)→#16a34aautonome dans view_ranking.html → test catch immédiatement la régression.- [3.4]
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_CSSetcomparison.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_hexstrippait 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_hexutilise désormaisast.parsepour 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 versvar(--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_PYne contient que__init__.py.Preuve anti-theater
Sabotage de
var(--g-200, #ccc)→#cccautonome à l'intérieur de_INLINE_CSSdansrender.py. Avant le fix du guard : test passait silencieusement (theater). Après : test échoue surligne 142 : #cccavec assertion exacte. Restauration → 59/59 verts (37 renderers + 17 templates + 5 top-level py).- [3.5]
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.cssmigré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 :
- Définitions de tokens hors
:root: les blocsbody.palette-classicetbody:not(.palette-classic)définissent--palette-good/warning/badavec 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). - 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). - Data URLs
data:image/svg+xml: leurs%23abc123URL-encoded contiennent des hex légitimes (contenu d'image SVG inliné, pas une couleur CSS). Guard ignore les lignes contenantdata: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.csspour 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/-midselon 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_templatesglob désormais aussi*.css._styles.css(2600 LOC) et150 LOC) couverts par le guard._design_tokens.css(Preuve anti-theater
Sabotage
color: var(--fern, #16a34a)→color: #16a34aligne 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 viapicarones demo --output /tmp/test.html.Sous-phase restante Phase 18 : 18d
_app.jsPALETTEengineColor()ranking ordering — TRÈS RISQUÉ (intestable sans browser).
- Définitions de tokens hors
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.csssymétrique6px 14px(était6px 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.jsperd la PALETTE Tailwind statique (10 hex vifs), gagne un_CHART_PALETTE(5 tokens) lu viagetComputedStyleau boot ;_ENGINE_RANK_MAPconstruit depuisDATA.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_htmldepuis view_analyses versengines_table.html. TriggerbuildCharts()étendu aux vues à charts. Nouvelle clé i18nh_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-docvers documents.html ;chart-quality-cer+pareto-chart(avec toolbar 3 axes + assumptions details) +marginal_cost_htmlvers crosses.html. 19 tests + 3 sabotages. Wrapper Pareto passe dechart-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 Pythonbuild_correlation_matrix_html- 10 hex CSS-in-JS hors palette). Clé i18n orpheline
corr_engine_labelretirée (parité 635/635). 7 tests + 3 sabotages.
- 10 hex CSS-in-JS hors palette). Clé i18n orpheline
- Phase 22+23 — Suppression onglet Analyses +
view_resultsen<details>:view_analyses.htmlsupprimé, bouton nav retiré,XER_VIEWSnettoyé.view_results_html(projections EvaluationView : TextView, AltoView) intégré en<details>repliable au bas deengines_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_htmlserait 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.
- X1 —
engineColor()self-healing contre race condition deeplink : bug runtime non détecté en Phase 20._routing.js(chargé avant_app.js) enregistre son handlerDOMContentLoadedqui s'exécute AVANT celui de_app.js. Sur deeplink#engines/#documents/#crosses,buildCharts()se déclenche →engineColor()lit_CHART_PALETTEencore 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 : unreplace(..., ..., 1)faible peut faire passer un sabotage à tort si la chaîne apparaît plusieurs fois ; et lesaria-labelsont résolus via i18n au rendu, pas via le renderer Python. 6 sabotages anti-theater validés. - X3 — Consolidation
margin-top: 1.25reminline : 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.activeet.xer-subpane.activesontdisplay: 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 :_switchViewaccédaitgetElementById(...).classListsans null check → crash sur hash invalide (#characters,#foo). Fix : garde + fallback silencieux vers overview viawindow.showXerView+console.debugpour traçabilité. Retrait de 6 fonctions JS dormantes (initCharView,renderCharView,renderConfusionHeatmap,renderLigatureDetail,renderTaxonomyDetail,showConfusionExamples), dulet charViewBuilt, decharacters:'caract'dans tabMap, et duif (name === 'characters'). ~225 LOC nettes retirées de_app.js. Bonus : 6 hex hardcodés inline dansrenderConfusionHeatmapretiré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 = 0faisait 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.pyqui vérifient la SÉMANTIQUE post-guard (assignation effective à_CHART_PALETTE, lookup derankutiliseengineName).Z3 — Theater par decoy dans le HTML rendu : les tests qui grep le HTML produit par
picarones demo(fixtureshtml_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 versoverviewsans crashengineColor()robuste contre prototype pollution (__proto__,constructor,toString) viaObject.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.RunOrchestratorexposé au niveau racine. Consomme unRunSpecPydantic validé et expose 4 fichiers JSONL natifs (run_manifest.json,pipeline_results.jsonl,artifacts_index.jsonl,view_results.jsonl) en plus duBenchmarkResultlegacy (viaspec.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 propresViewResulttypés avecmetric_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 unArtifact 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 viaextract_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-sectionrendue parpicarones/reports/html/renderers/view_results.py. Affiche un tableauMétrique × enginepar 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 sibenchmark.view_resultsvide (chemin legacy intact). BenchmarkResult.view_results: nouveau champ optionnel{view: {engine: {doc: {metric: value}}}}peuplé par le converter depuis leRunResult.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_servicesupprimée — utilisezRunOrchestrator+prepare_preset_args(Python) ouRunOrchestrator.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.pytests/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_namepropageexpose_altoà Tesseract.
- Web :
BenchmarkRunRequestétendu avecviews: list[ViewName],profile,partial_dir,entity_extractor,output_json.PipelineConfig.expose_altoactivable par concurrent. Le workerrun_benchmark_thread_v2propage les nouveaux champs au pattern 3 étapes. - Helper test :
tests/_migration_helpers.run_via_orchestratorreçoit un kwargviews(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_cliqui mutualise le pattern 3 étapes. Comportement utilisateur identique.picarones.interfaces.web.benchmark_utils.run_benchmark_thread_v2: pattern 3 étapes inline. L'API RESTPOST /api/benchmark/runest inchangée pour les clients.picarones.app.services.__init__expose désormaisPresetArgs,prepare_preset_args,run_result_to_benchmark_resultcomme API publique.TesseractAdapter.output_types: étendu de{RAW_TEXT, CONFIDENCES}à{RAW_TEXT, CONFIDENCES, ALTO_XML}(set maximal). Les pipelines existants restent inchangés tant queexpose_alton'est pas activé.build_text_view/build_search_view: nouveau kwargchar_excludepropagé jusqu'auDefaultEvaluationViewExecutorqui 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 vialegacy_runner_compat→ Checkpoint 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_METRICS→ Checkpoint 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 shimlegacy_runner_compatet 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/startretiré au profit dePOST /api/benchmark/run. Le modèle PydanticBenchmarkRequest(liste de moteurs plats) est remplacé parBenchmarkRunRequest(liste dePipelineConfig). Helpers associés supprimés :_legacy_request_to_run_request,run_benchmark_thread(v1 ;run_benchmark_thread_v2reste). 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 parCorpusRunner.max_in_flightdirectement.expand_legacy_keys()retiré depicarones.domain.artifacts— 0 caller en production. Le dictLEGACY_VALUE_ALIASESreste vivant pour le canonicalisation des manifests legacy.JSON
BenchmarkResultpré-v2.0 plus relisibles — le retrait deexpand_legacy_keysinterdit le round-trip depuis des sorties v1.x. Régénérer les benchmarks de référence.Paramètre
text_hintretiré depicarones.evaluation.synthetic._make_placeholder_png(était jamais lu).
Added — features inachevées débloquées
Agrégation sur-normalisation LLM corpus-wide :
aggregate_over_normalizationcâblée via@register_corpus_aggregator(name="over_normalization", ...)(profilsphilological,diagnostics,full). Le hook extrait depuisDocumentResult.pipeline_metadata["over_normalization"]et alimenteEngineReport.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_fallbackest 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.yamlversionné 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.
- CLI :
register_default_metrics()exposée publiquement — remplace le side-effectimport picarones.evaluation.metricsopaque en tête depicarones/__init__.py. Idempotente (sys.modulescache). Auto-déclenchement préservé pour rétrocompat.RunResultaccessible depuispicarones.pipeline.run_result— déplacé deapp.results(compat shim conservé) vers la couche 4 pour respecter l'orientation des couches (reports/ne peut plus importer depuisapp/).
Changed — architecture & lisibilité
- 8
__init__.pyde couche : renumérotationCercle 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). Budgettest_file_budgetsresserré 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).PipelineModeunifié : source uniquepicarones.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_dirdansinterfaces/web/_path_helpers.py; 2 routers migrés. - eScriptorium anti-SSRF :
_get,_postet le téléchargement d'image utilisent désormaisvalidate_http_urletdownload_url(cohérence avec IIIF/Gallica/HTR-United). - Tesseract
langvalidation : 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-countersexécutescripts/gen_readme_tables.py --check. - CI : 7 nouveaux tests d'architecture verrouillent les
invariants pour bloquer la régression :
test_no_zombie_skips.py(interditpytest.skipsur dep obligatoire).test_no_legacy_imports_in_rewrite.pyrefondu — test actif contre la résurrection des paquets legacy supprimés (au lieu d'unLEGACY_PACKAGES = ()vacuement vrai).test_no_broad_pytest_raises.py— refuse lespytest.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 danstests/integration/live/porte@pytest.mark.live.test_reports_layer_strict.py— interdit les importsreports/ → {adapters, app, interfaces}.test_pipeline_mode_single_source.py— refuse toute nouvelle redéfinition deLiteral["text_only", "text_and_image", "zero_shot"].test_api_stable_modules_exist.py— chaque module cité dansapi-stable.mddoit s'importer.
Removed
POST /api/benchmark/start+ helpers (cf. BREAKING).BenchmarkRequestmodèle Pydantic v1 (cf. BREAKING).max_workers,text_hintparamètres morts.expand_legacy_keys()(cf. BREAKING).- 5 helpers
_apply_*+_degrade_pure_pythondans robustness.py (~300 LOC). - 7
pytest.skip("click non installé")zombies danstests/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:app→picarones.interfaces.web.app:app).- 8
except: passsilencieux : tous remplacés parlogger.warning("[<module>] ...")oulogger.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 enFrozenInstanceErroroupydantic.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
# nosecet commentaire de justification (sites SQL où lesfieldsinterpolés sont des littéraux internes, valeurs via?). - eScriptorium : urlretrieve sans validation →
download_urlavec 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
KrakenAdapteretCalamariAdapter(couche 5) avec lazy imports. Auparavant annoncés par/api/enginesmais 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_objectrestaurent 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/editiongénèrent désormais le rapport HTML automatiquement à côté du JSON (--no-htmlpour 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/loadqui 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_demoexposé 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 utilisefrom_remote(timeout=5)avec fallback automatique sur démo (au lieu defrom_demo()exclusif). Variable d'envPICARONES_HTR_UNITED_OFFLINE=1pour 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_jobreç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_v2après conversionBenchmarkRequest → BenchmarkRunRequest. Marqué deprecated dans les logs ; un seul chemin à patcher.- CLI
engines: source de vérité unique avecadapters/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
CompetitorConfig→PipelineConfig(renommage de classe).PipelineConfig.ocr_engine→engine_name: le field accepte aussicorpus(OCR pré-calculé) et des VLMs en zero-shot — le préfixeocr_était trompeur. Les clients qui envoyaient{"ocr_engine": "…"}doivent migrer vers{"engine_name": "…"}(Pydantic v2 ignore silencieusement l'extra → benchmark refuse).PipelineConfig.pipeline_modetypéLiteral["text_only", "text_and_image", "zero_shot"]. Toute autre valeur (y compris les anciens aliaspost_correction_text/post_correction_image) est rejetée en 422 par Pydantic — fini le fallback silencieux verstext_onlyqui masquait les configs invalides.
Security
output_dirvalidé (validated_pathcontrecompute_workspace_roots) dansapi_htr_united_importetapi_huggingface_import— plus de path traversal écriture.db_pathvalidé dans/api/history/regressions— plus de lecture SQLite arbitraire. Pour pointer une base hors workspace, exporterPICARONES_HISTORY_DB.- ZIP collision de basename :
a/img.png+b/img.pngne 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_json↔from_jsonpré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.0dans 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, evaluationpicarones/measurements/— Lot D + E : evaluation/metrics, statisticspicarones/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 parpicarones.adapters.ocr.*(BaseOCRAdapternatif, factoryocr_adapter_from_name).picarones/adapters/legacy_pipelines/(Sprint H.2.c) :OCRLLMPipeline,PipelineMode. Remplacés parpicarones.pipeline.llm_pipeline_config.OCRLLMPipelineConfigpicarones.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ésinterfaces/cli/{run,report,import_corpus}.py(Sprint H.4) : consolidé eninterfaces/cli/(16+ commandes Click).picarones/interfaces/web/_legacy/+ stubs canoniques inachevésinterfaces/web/{__init__,app,security}.py+routers/,templates/,static/,i18n/(Sprint H.4) : consolidé eninterfaces/web/(FastAPI + UI Jinja2 + SSE benchmark + ZIP upload).
Renames
picarones/reports_v2/→picarones/reports/(Sprint H.3).picarones/app/services/_legacy_runner_adapter.py→picarones/app/services/benchmark_runner.py(Sprint H.4) : drop le préfixe_legacy_; c'est l'entry point public des interfaces versBenchmarkService.picarones/app/services/_legacy_partial_store.py→picarones/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 GTENTITIES.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èveValueErroravant tout calcul.
Architecture
LEGACY_PACKAGES = ()danstests/architecture/test_no_legacy_imports_in_rewrite.py: plus aucun paquet legacy.test_legacy_canonical_parity.pysupprimé (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.