# Plan de remédiation — Picarones vers le niveau BnF / British Library > Réponse opérationnelle à > [`institutional-readiness-2026-05.md`](institutional-readiness-2026-05.md) > (13 BLOCKERS, 28 MAJORS, 18 MINORS, 1 faux positif). > Cible : 0 BLOCKER, 0 MAJOR ouvert ; CITATION valide ; audit RGAA AA passé ; > `pip install picarones` fonctionnel ; lock file utilisé en prod. > > **15 sprints, 8 phases, ~58 PJ, ~12 semaines en 1 ETP.** > Avec 2 ETP et la parallélisation décrite §2 : ~7–8 semaines. --- ## 1. Principes directeurs Six principes structurent le séquençage. Chacun est tenu sur **toute** la durée du plan, pas seulement à un sprint isolé. 1. **Scaffolding avant contenu.** Les *garde-fous CI* (Phase 0) sont posés en premier pour que les sprints suivants ne puissent pas régresser sur ce qu'on vient de corriger. Sans cela on déboguerait des régressions au lieu d'avancer. 2. **Tests avant fixes.** Chaque correctif d'audit s'accompagne d'un test de non-régression dans le même PR. Un fix sans test est une dette qui se rouvrira au sprint suivant. 3. **DRY entre code et documentation.** Une assertion vérifiable (compteur de tests, liste des moteurs, liste des commandes CLI) doit être *générée* depuis le code, pas dupliquée à la main. Le sprint A2 pose ce principe pour le README et l'audit. 4. **Source unique de vérité.** À chaque divergence entre deux documents (CLAUDE.md vs README vs SPECS), on désigne le canon et on déprécie le reste. Pas de patch parallèle. 5. **Refonte documentation produit en dernier.** Le README et SPECS reflètent un état du code. On ne refait pas ces documents tant que les phases 1–6 ne sont pas stabilisées, sinon on les refait deux fois. 6. **Parallélisation contrôlée.** Les phases 3 (accessibilité) et 4 (reproductibilité opérationnelle) touchent des fichiers disjoints : templates HTML / CSS / JS d'un côté, Dockerfile / pyproject / workflows GitHub de l'autre. Elles peuvent tourner en parallèle si l'équipe a deux ETP. Avec un seul ETP, séquentiel. --- ## 2. Vue d'ensemble — 15 sprints en 8 phases ### Tableau récapitulatif | Phase | Sprint | Thème | PJ | Sem. (1 ETP) | Items audit | |---|---|---|---|---|---| | 0 | A1 | Hardening CI | 4 | 1 | B-7, B-8, M-4, M-15, m-7, m-8, m-9 | | 0 | A2 | Tests de cohérence documentation | 3 | 2 | (préparation A13, m-12) | | 1 | A3 | Refactor cercles + importers | 3 | 3 | B-1, B-2, B-3, m-17 | | 2 | A4 | Sécurité web (CSRF, /health) | 3 | 4 | B-11, M-3 | | 2 | A5 | Concurrence et performance | 5 | 4–5 | M-13, M-14, M-16, m-10 | | 3 | A6 | WCAG niveau A (bloquant) | 3 | 5–6 | B-9, B-10, m-3, m-4 | | 3 | A7 | WCAG AA + i18n résiduel + a11y statement | 3 | 6 | m-1, m-2, m-5, m-6, M-9 | | 4 | A8 | Reproductibilité opérationnelle | 3 | 5–6 *(parallèle phase 3)* | M-1, M-2, M-12, M-18, m-11, m-13, m-14 | | 4 | A9 | Distribution PyPI + ghcr.io + releases | 3 | 7 | M-5, M-6, m-15, m-16 | | 5 | A10 | Politiques de gouvernance | 2 | 8 | M-10, M-11 | | 5 | A11 | Documentation institutionnelle | 5 | 8–9 | M-7, M-8, M-17 | | 6 | A12 | Publication scientifique | 5 *(+ peer review externe)* | 9–10 | B-4, B-5, B-6 | | 7 | A13 | Refonte README | 4 | 10–11 | B-13, M-19 à M-28, m-18, §9.3 | | 7 | A14 | Refonte SPECS.md | 3 | 11 | B-12 | | 8 | A15 | Audits externes (RGAA + sécurité) | 1 *(+ cycle externe)* | 11–12 | validation finale | | 4* | A16 | Build Docker reproductible (digest + lock file) | 1 | *(post-A14)* | M-2 (clôture) | | **TOTAL** | | | **~59 PJ** | **~12 sem. (1 ETP)** | **60 items** | > Note : Sprint A16 a été ajouté après l'exécution d'A14 pour clôturer > M-2 bout-en-bout (digest sha256 sur les deux ARG + lock file > ``requirements-docker.lock`` consommé par le Dockerfile). La dette > résiduelle ``apt-get`` non figé est tracée comme nouvel item M-29 > dans `institutional-readiness-2026-05.md` (différé post-v1.2). ### Diagramme de Gantt synthétique ``` Sem. 1 2 3 4 5 6 7 8 9 10 11 12 Phase 0 ████░░ ← CI hardening + doc consistency tests (gates) Phase 1 ███ ← refactor cercles + importers Phase 2 █████ ← web sécurité + concurrence/perf Phase 3 ██████ ← a11y niveau A puis AA Phase 4 █████ ← reproductibilité ops + distribution (parallèle) Phase 5 ████ ← gouvernance + doc institutionnelle Phase 6 █████ ← CITATION + JOSS draft ════════════════════ ← peer review externe (calendrier propre) Phase 7 █████ ← refonte README + SPECS Phase 8 ███ ← audits externes ════ ← audit RGAA externe (calendrier propre) ``` Légende : `█` = sprint actif (1 ETP) ; `═` = calendrier externe en parallèle. ### Dépendances dures - **A1 doit précéder tout** : sans seuil de couverture, scanners de sécurité et timeout pytest, les sprints suivants ne peuvent pas être validés en CI. - **A2 doit précéder A13/A14** : les tests de cohérence documentation posés en A2 deviennent les *gates* qui valident la refonte README/SPECS. - **A3 doit précéder A12** : les violations Cercle 2→3 doivent être réparées avant que SPECS et le papier JOSS décrivent l'architecture. - **A8 doit précéder A12** : le snapshot de reproductibilité doit être documenté avant qu'un papier puisse promettre la reproductibilité. - **A9 doit précéder A12** : un papier qui pointe vers `pip install picarones` exige que ça fonctionne. - **Phases 1–6 doivent précéder Phase 7** : on ne refait pas le README tant que le code n'est pas stable. ### Dépendances souples (parallélisables avec 2 ETP) - A6+A7 (a11y) ⫽ A8+A9 (ops/distribution) — fichiers disjoints. - A11 (doc institutionnelle) peut commencer dès la fin de A8. - A12 démarre dès que A3+A8+A9 sont clos ; sa rédaction peut chevaucher A13/A14. --- ## 3. Phase 0 — Garde-fous (sem. 1–2) > **Objectif** : aucun sprint ultérieur ne peut faire régresser ce qu'on > a corrigé, parce que la CI le détecte au PR. Sans cette phase, > chaque correctif est susceptible de se rouvrir trois sprints plus tard. ### Sprint A1 — Hardening CI (4 PJ) **Pourquoi à cette position dans la séquence** Les phases suivantes corrigent des dizaines de fichiers. Sans seuil de couverture, sans scanners de sécurité, sans type-check, sans timeout pytest et sans validation pre-commit en CI, chaque correctif est une prise de risque silencieuse : on apprend la régression à la PR suivante, voire en production. C'est le sprint qui rend tous les autres exécutables de manière crédible. **Items de l'audit résolus** **B-7** (scanners sécurité CI), **B-8** (cov-fail-under), **M-4** (mypy en CI), **M-15** (pytest-timeout), **m-7** (pre-commit non rejoué en CI), **m-8** (Python 3.13 manquant de la matrice), **m-9** (API stability defaults). **Livrables concrets** - `.github/workflows/ci.yml` : ajout d'un job `security` (bandit + pip-audit + trivy sur l'image Docker), ajout `--cov-fail-under=85` au job `tests`, ajout `pytest-timeout=300` global, ajout d'un job `typecheck` (mypy strict sur `picarones/core/`, lax ailleurs), ajout Python 3.13 à la matrice (mode warning, pas bloquant 6 mois). - `.github/workflows/precommit.yml` (nouveau) : rejoue tous les hooks pre-commit en CI pour empêcher le bypass `--no-verify`. - `pyproject.toml` : `[tool.mypy]` avec `strict = true` sur `picarones.core.*`, `[tool.pytest.ini_options]` `timeout = 300` et `timeout_method = "thread"`. Ajout `pytest-timeout` à `[dev]`. - `picarones/py.typed` (marqueur PEP 561 pour signaler le typage aux consommateurs externes). - `tests/core/test_public_api_signatures.py` : pour chaque fonction exposée par `picarones/__init__.py`, vérifier les valeurs par défaut des paramètres via `inspect.signature` (couvre **m-9**). **Critères d'acceptation** - [ ] `ruff check picarones/ tests/` passe (régression nulle). - [ ] `pytest tests/` passe avec `--cov-fail-under=85`. - [ ] `bandit -r picarones/ -ll` passe en CI. - [ ] `pip-audit --strict` passe en CI. - [ ] `mypy picarones/core/ --strict` passe. - [ ] Un PR qui supprime un default value d'une fonction publique fait échouer la CI sur `test_public_api_signatures`. - [ ] Un PR qui dépasse 5 minutes sur un test individuel fait échouer la CI avec un message explicite, pas un hang. **Risques et mitigation** - *Risque* : seuil de couverture initialement fixé trop haut, premier PR bloqué. *Mitigation* : mesurer le baseline avant de fixer le seuil (`pytest --cov` sur `main`), poser le plancher 2 points en dessous. - *Risque* : `mypy --strict` sur `core/` révèle 50 erreurs cachées. *Mitigation* : démarrer par `core/` qui est le plus stable et le mieux typé ; les autres cercles passent en mode `--no-strict-optional` pendant 1 sprint, durci en A11. --- ### Sprint A2 — Tests de cohérence documentation (3 PJ) **Pourquoi à cette position dans la séquence** L'audit §9 montre que README liste un moteur Kraken qui n'existe pas, documente des variables AWS sans adapter, annonce 1 242 tests au lieu de 3 356, et liste 9 commandes CLI au lieu de 15. Ces erreurs ne viennent pas d'incompétence — elles viennent du fait que le README n'a aucun *gardien* automatique. Si on refait le README en A13 sans poser ce garde-fou, il dérivera à nouveau dès le sprint suivant. A2 pose donc des **tests de cohérence** entre la documentation publiée et le code. Ces tests deviennent ensuite les *gates* qui valident A13 et A14. **Items de l'audit résolus** Préparation des **gates** pour M-19 (compteur tests), M-23 (moteurs annoncés), M-24 (variables env sans adapter), M-25 (CLI), M-26 (API web), M-27 (métriques), M-28 (sections rapport). **m-12** (numérotation sprint des tests). **Livrables concrets** - `tests/docs/test_readme_consistency.py` (nouveau) : - parse les tableaux Markdown du README ; - pour chaque moteur listé, vérifie qu'un fichier `picarones/engines/{nom_normalisé}.py` existe ; - pour chaque commande CLI listée, vérifie qu'elle apparaît dans `picarones --help` ; - pour chaque endpoint web listé, vérifie qu'il apparaît dans `app.openapi()["paths"]` ; - pour la phrase « pytest tests/ → N passed », vérifie que N correspond au baseline collecté par `pytest --collect-only`. - `tests/docs/test_specs_consistency.py` (nouveau) : même approche pour SPECS.md, avec acceptation explicite des sections marquées « Reporté » ou « Abandonné au profit de … » (lecture des balises). - `tests/docs/test_changelog_links.py` (nouveau) : vérifie que toute référence `Sprint N` dans CHANGELOG correspond à une entrée existante. - `Makefile` : cible `make doc-check` qui lance ces tests seuls (utile pour l'auteur du README). - `docs/developer/doc-consistency.md` (nouveau, ~80 L) : explique le contrat (« si vous ajoutez un moteur, ajoutez la ligne dans le tableau README ; le test la valide »). - `tests/__init__.py` ou `conftest.py` : helper qui audite la numérotation `test_sprintNN` et signale les trous suspects (couvre **m-12**, sortie informative pas bloquante). **Critères d'acceptation** - [ ] `pytest tests/docs/` passe sur l'état actuel des docs **après avoir corrigé le README et SPECS minimalement** (suppression simple des fausses promesses pour ne pas bloquer le présent sprint ; refonte complète en A13/A14). - [ ] Un PR qui ajoute un nouvel adapter OCR sans mettre à jour le tableau README échoue à `pytest tests/docs/test_readme_consistency.py`. - [ ] Un PR qui supprime une commande CLI sans mettre à jour le README échoue de la même manière. - [ ] `make doc-check` produit un rapport lisible en < 5 s. **Risques et mitigation** - *Risque* : les tests sont trop stricts et bloquent un PR légitime (ex : moteur en cours d'ajout, doc à jour ensuite). *Mitigation* : tolérer une « exception déclarée » via une balise HTML invisible `` dans le tableau, à utiliser avec modération et auditée à la PR. - *Risque* : la mise à jour minimale du README/SPECS pour faire passer le test interfère avec la refonte A13/A14. *Mitigation* : se limiter à *supprimer* les promesses fausses (Kraken ligne, AWS env vars), pas à *ajouter* du contenu nouveau — ça reste pour A13. --- ## 4. Phase 1 — Hygiène architecturale (sem. 3) > **Objectif** : ramener l'architecture à 100 % de conformité au modèle > 3 cercles avant que la documentation produit (Phase 7) ou un papier > JOSS (Phase 6) ne décrivent l'architecture. ### Sprint A3 — Refactor cercles + importers (3 PJ) **Pourquoi à cette position dans la séquence** L'audit §2 identifie deux violations Cercle 2 → Cercle 3 (`measurements/statistics.py:861` importe `report.diff_utils` ; `measurements/difficulty.py:195` importe `report.colors`) et trois `except Exception: pass` qui violent la règle propre du projet. Ces dettes architecturales sont locales mais doivent être payées *avant* la Phase 6 : un papier JOSS qui décrit une architecture en 3 cercles ne peut pas se faire mentir par le code. De plus, c'est **le sprint le moins risqué** des phases suivantes — il rode l'équipe sur le pattern *fix + test de non-régression* posé par A1. **Items de l'audit résolus** **B-1** (statistics.py:861 → core/), **B-2** (difficulty.py:195 → report/), **B-3** (3 importers `except Exception: pass`), **m-17** (`tests/measurements/test_sprint11_i18n_english.py` importe Cercle 3 → déplacement en `tests/integration/`). **Livrables concrets** - Création de `picarones/core/diff_utils.py` (déplacement depuis `picarones/report/diff_utils.py`). `picarones/report/diff_utils.py` devient un ré-export trivial pour rétrocompat. `statistics.py:861` importe désormais depuis `core`. - Création de `picarones/report/difficulty_render.py` qui contient `difficulty_color()`. `picarones/measurements/difficulty.py` ne contient plus que la logique numérique. - `picarones/extras/importers/huggingface.py:266, 416` : remplacer les deux `except Exception: pass` par `logger.warning("[importers/hf] a échoué (mode dégradé) : %s", e)`. - `picarones/extras/importers/htr_united.py:448` : idem. - Émettre un `Fact` `IMPORTER_FALLBACK_TRIGGERED` (priorité MEDIUM, template factuel sans chiffres en dur) pour que la synthèse du rapport mentionne l'incident à l'utilisateur final. - Déplacement de `tests/measurements/test_sprint11_i18n_english.py` vers `tests/integration/test_sprint11_i18n_english.py` (couvre **m-17**). - Déplacement de `tests/measurements/test_sprint94_error_absorption.py` vers `tests/integration/` (audit §2 MINOR 4). - Tests de non-régression : - `tests/core/test_diff_utils.py` : reproduire les tests de `tests/report/test_diff_utils.py` au nouveau chemin (le doublon sur `report/` reste pour la rétrocompat). - `tests/measurements/test_difficulty_pure.py` : vérifier que `picarones.measurements.difficulty` n'importe plus rien depuis Cercle 3 (`importlib.util.find_spec` + analyse AST). - `tests/extras/test_importer_warnings.py` : vérifier que chaque chemin d'erreur des 3 importers loggue un warning explicite (capturer via `caplog`). - Garde-fou architectural : `tests/core/test_circle_dependencies.py` (nouveau) qui parse les imports de tous les fichiers `picarones/measurements/`, `engines/`, `llm/`, `pipelines/`, `modules/` et **échoue** si l'un d'eux importe `picarones.report.*`, `picarones.cli.*`, `picarones.web.*`, `picarones.extras.*`. **Critères d'acceptation** - [ ] `pytest tests/` passe (régression nulle ; rappel : 3 356 baseline). - [ ] `tests/core/test_circle_dependencies.py` rapporte 0 violation. - [ ] `grep -rn "except Exception:$" picarones/extras/importers/` renvoie 0 ligne (les 3 violations B-3 sont remplacées). - [ ] Le rapport HTML d'un benchmark où un fallback importer a été déclenché contient le `Fact` `IMPORTER_FALLBACK_TRIGGERED` dans la synthèse narrative. **Risques et mitigation** - *Risque* : la fonction `compute_word_diff` déplacée a des callers externes non documentés. *Mitigation* : laisser un ré-export dans `picarones/report/diff_utils.py` avec un `DeprecationWarning` au premier appel (suppression planifiée 2 versions plus tard). - *Risque* : le test `test_circle_dependencies` refuse un import légitime (par exemple un test qui mocke). *Mitigation* : limiter le test à `picarones/`, pas à `tests/` ; ne scanner que les imports top-level, pas les imports paresseux dans des fonctions (qui sont acceptables pour break dependency cycles). --- ## 5. Phase 2 — Robustesse runtime (sem. 4–5) > **Objectif** : durcir l'application web et l'orchestrateur de benchmark > avant de pouvoir promettre une adoption institutionnelle. Une bibliothèque > nationale ne déploie pas un service qui n'a ni protection CSRF, ni > endpoint `/health`, ni test de concurrence. ### Sprint A4 — Sécurité web (3 PJ) **Pourquoi à cette position dans la séquence** A1 a posé les scanners (bandit + trivy) qui détecteront toute régression sécurité. A3 a stabilisé l'architecture. Il est maintenant temps de fermer les deux trous fonctionnels : **B-11** (pas de CSRF) et **M-3** (le `HEALTHCHECK` Docker pointe vers un `/health` qui n'existe pas). Ces deux items sont indépendants des autres phases — on les fait avant A5 (concurrence) parce qu'A5 va ajouter des tests d'intégration qui ont besoin du middleware CSRF stabilisé. **Items de l'audit résolus** **B-11** (CSRF), **M-3** (`/health` absent). **Livrables concrets** - `picarones/web/security.py` : ajout du middleware `csrf_middleware` basé sur `starlette-csrf` (ou implémentation maison ~80 L : token signé HMAC-SHA256 dans cookie `picarones_csrf` + en-tête `X-CSRF-Token` exigé sur POST/PUT/DELETE). Activé par variable `PICARONES_CSRF_REQUIRED=1`. Désactivé par défaut sur Space public (pas de session authentifiée à protéger), activé d'office en mode institutionnel. - `picarones/web/routers/system.py` : ajouter `GET /health` qui retourne `{"status": "ok", "version": __version__}` en < 50 ms, sans toucher à la BD ni aux engines (vrai healthcheck Kubernetes-ready). Conserver `/api/status` qui reste plus riche (pour le frontend). - `Dockerfile:96` : pointer le `HEALTHCHECK` vers `/health` au lieu de `/api/status`. - `picarones/web/templates/_app.js` : pour chaque appel `fetch` POST, injecter automatiquement l'en-tête `X-CSRF-Token` lu depuis le cookie. - `tests/web/test_csrf.py` (nouveau, ~12 cas) : POST sans token → 403 ; POST avec token invalide → 403 ; POST avec token valide → 200 ; en mode public (sans `PICARONES_CSRF_REQUIRED`) → POST passe sans token (rétrocompat HF Space). - `tests/web/test_health.py` (nouveau, ~4 cas) : `GET /health` → 200 + JSON valide ; latence < 100 ms ; ne déclenche pas de log SQL ; fonctionne même si la BD jobs est down. - Documentation : `SECURITY.md` mise à jour avec un encart « Mode institutionnel : exporter `PICARONES_CSRF_REQUIRED=1` derrière votre reverse-proxy ». **Critères d'acceptation** - [ ] `pytest tests/web/` passe. - [ ] `curl -X POST -H 'X-CSRF-Token: invalid' http://localhost:7860/api/config/save` retourne 403 quand `PICARONES_CSRF_REQUIRED=1`. - [ ] `docker run … && sleep 35 && docker inspect | grep -c '"Status": "healthy"'` retourne 1 (le HEALTHCHECK passe). - [ ] `bandit -r picarones/web/` ne signale aucune nouvelle issue. **Risques et mitigation** - *Risque* : une intégration tierce (jq, script CI maison) qui appelle `/api/benchmark/start` sans token casse en mode institutionnel. *Mitigation* : documenter explicitement dans `SECURITY.md` la procédure « générer un token via `GET /api/csrf/token` puis le passer dans toutes les requêtes » + exemple curl. - *Risque* : l'ajout du middleware ralentit les requêtes. *Mitigation* : benchmark p99 sur `/api/status` avant/après ; ajouter au job `tests` CI un check `< 200 ms p99 sur 100 requêtes`. --- ### Sprint A5 — Concurrence et performance (5 PJ) **Pourquoi à cette position dans la séquence** L'orchestrateur (`measurements/runner.py`, 1 019 lignes) gère ProcessPool + ThreadPool, et la couche web utilise SSE + SQLite WAL. L'audit §3 (M-13, M-14, M-16) signale qu'aucun test ne couvre les cas concurrence sous charge, qu'il n'existe pas de garde-fou anti-régression de performance, et que les rapports HTML peuvent dépasser 200 MB sur de gros corpus. Tant que ces trois trous ne sont pas fermés, la promesse « plateforme robuste » est invérifiable. A5 vient après A4 parce que les tests de concurrence appellent l'API web et ont besoin du middleware CSRF stable. **Items de l'audit résolus** **M-13** (tests concurrence runner + SSE), **M-14** (anti-régression CER), **M-16** (lazy loading rapports), **m-10** (tests cloud OCR sur erreurs HTTP 429/401/503). **Livrables concrets** - `tests/integration/test_runner_concurrency.py` (nouveau, ~50 cas) : - 32 jobs concurrents avec `PICARONES_MAX_CONCURRENT_JOBS=32` ; - épuisement du ProcessPool puis recovery ; - mort d'un worker au milieu d'un benchmark ; - timeout d'un doc dans un workers (le runner doit isoler) ; - écritures SSE concurrentes sur la même file ; - réception `Last-Event-ID` après reconnexion → replay correct. - `tests/web/test_sqlite_concurrent_writes.py` (nouveau, ~10 cas) : 10 threads écrivent simultanément dans `JobStore` → 0 corruption, pas de `SQLITE_BUSY` qui remonte au client. - `tests/web/test_public_mode_hot_swap.py` (nouveau) : passage à chaud `PICARONES_PUBLIC_MODE=0 → 1` au milieu d'un benchmark ne casse pas les jobs en cours. - `tests/engines/test_cloud_http_errors.py` (nouveau, ~12 cas par cloud) : Mistral OCR / Google Vision / Azure DI mockés pour retourner 429 (rate limit), 401 (clé invalide), 503 (indisponible), réponse vide. Vérifier le retry exponentiel + le warning explicite. - `tests/fixtures/reference_corpus/` (nouveau) : 10 documents libres de droits couvrant 3 strates (médiéval, imprimé ancien, moderne) avec GT manuelle. Source : Gallica + Wikisource (à vérifier les licences doc par doc). - `.github/workflows/perf_regression.yml` (nouveau) : workflow hebdomadaire (cron) qui lance `picarones run` sur le corpus de référence avec Tesseract + Pero, échoue si CER > 15 %. **Pas** à chaque PR (coût) mais audit hebdo + rapport en GitHub Issue auto. - `picarones/report/generator.py` : nouveau paramètre `lazy_images: bool = False` (défaut conservateur). Si activé, externalise les images dans `report-assets/{doc_id}.png` à côté du HTML, avec ``. Le HTML reste auto-portant si on copie aussi le dossier. - `picarones/cli/__init__.py` : option `--lazy-images` sur `picarones report` qui propage le paramètre. - Documentation dans `docs/user/reading-a-report.md` : encart « Pour les corpus > 50 documents, activer `--lazy-images` ». **Critères d'acceptation** - [ ] `pytest tests/integration/test_runner_concurrency.py` passe en < 3 min sur le runner CI. - [ ] `pytest tests/web/test_sqlite_concurrent_writes.py` passe sans flakiness sur 10 runs successifs. - [ ] Le job `perf_regression` hebdomadaire publie un commentaire automatique sur une GitHub Issue dédiée (`#perf-baseline`) avec le CER mesuré. - [ ] Un rapport généré avec `--lazy-images` sur le corpus de référence pèse < 5 MB (vs ~50 MB sans). - [ ] Aucun test n'a un timeout > 60 s individuel (limite douce pour parallélisation pytest-xdist plus tard). **Risques et mitigation** - *Risque* : le corpus de référence introduit un coût CI permanent. *Mitigation* : ne le lancer qu'en hebdo (cron), pas en PR. Le job est skippable manuellement via `[skip perf]` dans le commit message pour les release urgentes. - *Risque* : `--lazy-images` casse l'auto-portance promise du rapport. *Mitigation* : documenter explicitement (encart en tête du rapport généré avec ce flag) et ajouter un sous-flag `--bundle-zip` qui produit un `.zip` contenant HTML + dossier d'images. --- ## 6. Phase 3 — Accessibilité (sem. 5–6, parallélisable avec Phase 4) > **Objectif** : atteindre WCAG 2.1 niveau A bloquant (A6) puis niveau > AA cible BnF (A7). Cette phase ne touche que les templates HTML/CSS/JS > et les fichiers i18n — fichiers disjoints de Phase 4. Avec 2 ETP, > A6+A7 et A8+A9 tournent en parallèle. Avec 1 ETP, A6 → A7 → A8 → A9. ### Sprint A6 — WCAG niveau A bloquant (3 PJ) **Pourquoi à cette position dans la séquence** A1 a posé les outils CI mais aucun n'audite l'accessibilité. A4 a durci les endpoints. Il est maintenant temps de fermer les **deux violations de niveau A** identifiées par l'audit §3 : graphiques Chart.js Canvas inaccessibles aux lecteurs d'écran (B-9) et absence de lien « Aller au contenu » (B-10). Sans ces deux corrections, **aucune déclaration de conformité RGAA n'est légalement possible**. Le sprint ouvre aussi la voie pour A11 (declaration d'accessibilité) qui ne peut pas être rédigée sans audit niveau A passé. **Items de l'audit résolus** **B-9** (Canvas charts inaccessibles, ~12 graphiques Chart.js), **B-10** (skip-to-content), **m-3** (bouton « Réinitialiser » sans clé i18n), **m-4** (tableaux HTML sans `scope="col"` sur ``). **Livrables concrets** - `picarones/report/templates/_app.js` : pour chaque instanciation Chart.js (`new Chart(canvas, …)` lignes 1062, 1102, et autres) : - ajouter `aria-label` descriptif sur le `` ; - générer en parallèle un `` masqué visuellement (`.visually-hidden`) avec les mêmes données, lié au canvas par `aria-describedby` ; - ajouter un bouton « Voir les données » qui révèle la table à tous (utile aussi pour la copie). Bouton masqué par défaut. - `picarones/report/templates/_header.html` : en premier enfant du ``, ajouter ``. Ajouter `id="main"` sur le `
` du `base.html.j2`. - `picarones/report/templates/_styles.css` : classe `.skip-link` cachée hors `:focus` (`position:absolute; left:-9999px;` → revient à `top:0; left:0;` au focus, contraste AA). - Tableaux dans `view_*.html` : ajouter `scope="col"` sur tous les `
` du tableau classement, du tableau NER, du tableau philologie, du tableau levers, etc. (~12 tables totales). - Bouton « Réinitialiser » (`_header.html:25`) : remplacer le label hardcodé par `{{ i18n.reset_all }}`. Ajouter la clé dans `picarones/report/i18n/{fr,en}.json`. - `picarones/report/i18n/fr.json` et `en.json` : nouvelle clé `skip_to_content` (« Aller au contenu » / « Skip to content ») et `reset_all` (« Réinitialiser » / « Reset all »). - `tests/report/test_a11y_level_a.py` (nouveau, ~15 cas) : - chaque rapport HTML généré (demo + corpus de référence) contient `