From 52a104be30a7afb83d411fa2fb875556f9eca692 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sat, 6 Jun 2026 09:55:18 +0200 Subject: [PATCH 1/3] Avoid mutating passed-in request data Convert and copy incoming request payloads to prevent accidental mutation of caller-provided mappings. In mediawiki_api_call, kwargs['data'] is wrapped with dict(...) so updates (like adding format) work for non-dict mappings and don't modify the original. In mediawiki_api_call_helper, data is shallow-copied before use to avoid side effects when modifying the payload. This makes API helper functions safer when callers pass in shared or immutable mapping types. --- wikibaseintegrator/wbi_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wikibaseintegrator/wbi_helpers.py b/wikibaseintegrator/wbi_helpers.py index 18e86f7e..2c5afc4e 100644 --- a/wikibaseintegrator/wbi_helpers.py +++ b/wikibaseintegrator/wbi_helpers.py @@ -66,6 +66,7 @@ def mediawiki_api_call(method: str, mediawiki_api_url: str | None = None, sessio # TODO: Add support for 'multipart/form-data' when using POST (https://www.mediawiki.org/wiki/API:Edit#Large_edits) if 'data' in kwargs and kwargs['data']: + kwargs['data'] = dict(kwargs['data']) if 'format' not in kwargs['data']: kwargs['data'].update({'format': 'json'}) elif kwargs['data']['format'] != 'json': @@ -165,6 +166,7 @@ def mediawiki_api_call_helper(data: dict[str, Any], login: _Login | None = None, """ mediawiki_api_url = str(mediawiki_api_url or config['MEDIAWIKI_API_URL']) user_agent = user_agent or (str(config['USER_AGENT']) if config['USER_AGENT'] is not None else None) + data = data.copy() hostname = urlparse(mediawiki_api_url).hostname if hostname is not None and hostname.endswith(('wikidata.org', 'wikipedia.org', 'wikimedia.org')) and user_agent is None: From e663c5443544f950f48a23bf0e4249f71806c815 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:58:14 +0200 Subject: [PATCH 2/3] Handle None data and add non-mutation tests data.copy() crashed when data was None (a case the helper explicitly handles) and when data was a Mapping without a copy() method. Use dict(data) instead, guarded against None. Add tests checking that mediawiki_api_call and mediawiki_api_call_helper leave the caller's payload untouched, accept a read-only mapping, and still accept data=None. Co-Authored-By: Claude Opus 5 --- .run/pytest in Podman.run.xml | 42 +++++++++++ TODO.md | 113 ++++++++++++++++++++++++++++++ test/test_wbi_helpers.py | 41 +++++++++++ wikibaseintegrator/wbi_helpers.py | 2 +- 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 .run/pytest in Podman.run.xml create mode 100644 TODO.md diff --git a/.run/pytest in Podman.run.xml b/.run/pytest in Podman.run.xml new file mode 100644 index 00000000..9b3d1494 --- /dev/null +++ b/.run/pytest in Podman.run.xml @@ -0,0 +1,42 @@ + + + + + + + \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..a9f551d7 --- /dev/null +++ b/TODO.md @@ -0,0 +1,113 @@ +# TODO — Revue du projet (2026-07-27) + +Fichier local, non versionné (ajouté au .gitignore). Issu de la revue complète du 27 juillet 2026. +État au moment de la revue : 203 tests OK (1,5 s), couverture 81 %, pylint 8.98/10, 41 issues ouvertes, dernière release 0.12.15 (21 déc. 2025). + +## 1. Quick wins (à faire en premier) + +- [x] **Fixer les 2 erreurs mypy sur master** — `wbi_login.py:339` et `wbi_login.py:428` : + `session.headers.get()` renvoie `str | bytes`, incompatible avec `dict[str, str]` attendu par `_perform_login()`. + Introduites par le bump mypy 2.3.0 (#1006). *(fait le 2026-07-27, garde `isinstance(…, str)`)* +- [x] **Corriger les `paths` de `.github/workflows/python-lint.yaml`** — ajouter `pyproject.toml` et `poetry.lock` + (comme `python-pytest.yaml`), sinon les bumps de dépendances mergent sans passer le lint (cause racine du point précédent). + *(fait le 2026-07-27)* +- [x] **Ajouter le marqueur `py.typed`** (PEP 561) — fichier vide `wikibaseintegrator/py.typed` + déclaration dans + `pyproject.toml`. Sans lui, les type hints ne profitent pas aux utilisateurs en aval. + *(fait le 2026-07-27, présence dans la wheel vérifiée via `poetry build`)* +- [x] **README** : supprimer/corriger la phrase « But WikibaseIntegrator lack the "fastrun" functionality » + (contradictoire, `wbi_fastrun.py` existe). *(fait le 2026-07-27 — texte identique à celui de la PR #1001 pour éviter un conflit de merge)* +- [x] **AGENTS.md** : la version n'est plus « mirrored in `__init__.py` » (lecture via `importlib.metadata` désormais). + *(fait le 2026-07-27)* + +→ Reste à faire pour cette section : commit + push (non fait, en attente de validation). + +## 2. Écluser la file d'attente (axe n° 1) + +- [ ] **Merger les 5 PRs perso ouvertes** : + - ~~#1001 — fix fastrun (`fix-wbi-fastrun`)~~ → **à fermer au profit de #333** (voir ci-dessous) + - #983 — SPARQL en POST body + - #982 — non-mutation des données passées en argument + - #981 — supprimer les sessions requests module-level + - #980 — AnonymousEditError + meilleure gestion d'erreurs +- [ ] **Publier la release 0.12.16** — 7 mois depuis 0.12.15, master contient déjà : récupération de session (#999), + abandon de mwoauth (#998), `check_constraints()` (#997), refactor entités (#996). +- [x] **Décision fastrun (2026-07-28) : retenir #333 (`new-fastrun`), fermer #1001.** + Vérifié : #333 = 235 tests, corrige le crash `wbi_fastrun.py:291` que #1001 laisse intact, et interroge SPARQL + par statement au lieu de la requête unique sur tout le corpus (celle qui a enchaîné cinq 504 WDQS au benchmark). + Coût : rebase nécessaire (conflits), rupture d'API fastrun (`get_items` → `get_entities`), donc changelog. +- [x] **Porter le meilleur de #1001 dans #333** → branche `port-1001-into-new-fastrun` (poussée, 253 tests). + `BaseDataType.from_sparql_value()` générique + implémentations pour Property, Lexeme, Form, Sense, GeoShape, + TabularData, CommonsMedia. Lève la limitation « datatypes non comparables » et corrige un faux appariement + silencieux de CommonsMedia (stockait l'URL Commons entière). +- [x] **Rebaser `new-fastrun` sur master + intégrer le portage** (fait le 2026-07-28). + PR #333 est passée de CONFLICTING à **MERGEABLE** : 6 commits, 0 de retard, 253 tests, mypy/isort/codespell/flynt + clean (les 2 erreurs mypy héritées ont disparu avec #1010). Un seul conflit, sur la phrase fastrun du README + (master via #1012 vs `new-fastrun`) — résolu en gardant celle de `new-fastrun`, plus précise. + Sauvegarde d'avant rebase : branche locale `new-fastrun-bkp-20260728` (`195ee1d`). +- [x] **#1001 fermée** (2026-07-28) avec un commentaire renvoyant vers #333 et détaillant la raison + (architecture de la requête en masse, crash `:291`, et ce qui a été porté). Branche `fix-wbi-fastrun` conservée. +- [x] **Branche `port-1001-into-new-fastrun` supprimée** (locale + distante) après vérification que son commit + est intégré à l'identique dans #333. +- [ ] **Relire et merger #333** — dernière étape du chantier fastrun. + Ne pas oublier : rupture d'API fastrun (`get_items` → `get_entities`, `write_required` gagne + `entity_filter`/`property_filter`) → entrée de changelog + note de version. + Après merge : supprimer la sauvegarde locale `new-fastrun-bkp-20260728` si tout va bien. +- [ ] **Grooming du backlog** (41 issues, les plus vieilles de 2023) : fermer l'obsolète, étiqueter le reste. +- [ ] **Bugs utilisateurs à prioriser** : + - #953 — KeyError `'hash'` au parsing des références + - #883 — erreur upload statement avec qualifier Time + - #836 — impossible d'écrire dans un MediaInfo vide +- [ ] Traiter les PRs externes en attente : #917 (merge multi-items), #926 (draft, défaut `Claims.add()`), #834, #663 (draft). + +## 3. Robustesse réseau + +- [ ] **Retry bloquant** dans `wbi_helpers.mediawiki_api_call()` : `max_retries=100` × `sleep(60)` = jusqu'à ~100 min + de blocage silencieux. Réduire les défauts + backoff progressif (le décorateur `wbi_backoff` ne couvre que SPARQL/login). +- [ ] **Lookups de datatypes fragiles** : le motif `[x for x in BaseDataType.subclasses if x.DTYPE == ...][0]` + (models/claims.py:127, wbi_fastrun.py, wbi_helpers.py) lève une `IndexError` brute sur datatype inconnu. + → registre avec message d'erreur explicite listant les datatypes supportés. +- [ ] (Long terme, si demande) API async via httpx pour les bots de masse. + +## 4. Architecture / dette + +- [ ] **Boilerplate propriétés** : centaines de getters/setters triviaux (baseentity.py, claims.py…). + → dataclasses (cf. branche dormante `implement-pep557`) ou suppression des propriétés triviales. +- [ ] **Imports cycliques** (pylint R0401) : `wikibaseintegrator ↔ entities ↔ wbi_helpers ↔ wbi_login`, + masqués par des imports locaux dans les fonctions. +- [ ] **État global restant** : dict `config` mutable sans validation, cache module-level `properties_dt` + (format2wbi), liste globale `fastrun_store`. (#981/#982 traitent déjà les sessions et la mutation.) +- [ ] **`format2wbi` / `_json2datatype`** : expérimentaux, non testés, références non attachées. + → finir ou sortir du module principal. + +## 5. Tests + +- [ ] **`wbi_helpers.py` à 69 %** : la logique de retry (rate limiting, readonly, maxlag) est en `pragma: no cover` + alors qu'elle est testable offline avec requests-mock. Code critique pour un bot. +- [ ] Modules les moins couverts : monolingualtext (70 %), globecoordinate (72 %), baseentity (73 %), references (77 %). + +## 6. Outillage / packaging + +- [ ] **ruff** : remplacerait isort + flynt et une partie de pylint (une config, beaucoup plus rapide en CI). +- [ ] **pre-commit** : la branche `add-pre-commit` existe déjà, jamais mergée. +- [ ] **PEP 621** : migrer `[tool.poetry]` vers `[project]` (supporté par Poetry 2). +- [ ] **Couverture en CI + badge** (issue #895). +- [ ] Classifieurs `pyproject.toml` : `Python :: 3.15` déclaré alors que 3.15 n'est pas sorti ; + `Development Status :: 4 - Beta` ne reflète plus la maturité. + +## 7. Stratégique + +- [ ] **API REST Wikibase** (issue #518) : direction poussée par Wikimedia. Au minimum une exploration de design + (module `wbi_rest` parallèle plutôt que réécriture). +- [ ] **Ergonomie 0.13** (avec dépréciations propres) : + - défaut `REPLACE_ALL` de `Claims.add()` (piège connu, cf. #926 / #841) + - confusion `len(claims)` (nb propriétés) vs `count()` (nb claims) + - supprimer la classe dépréciée `BColors` +- [ ] **Docs** : README de 35 Ko monolithique → déplacer les guides vers Sphinx/RTD, garder un README court. + +## Ordre de priorité suggéré + +1. Section 1 (quick wins) — ~1 h de travail, débloque la CI. +2. Section 2 — merges + release 0.12.16. +3. Bugs utilisateurs (#953, #883, #836) + grooming. +4. Sections 3–6 au fil de l'eau. +5. Section 7 ensuite. diff --git a/test/test_wbi_helpers.py b/test/test_wbi_helpers.py index fbad77e0..1d16152f 100644 --- a/test/test_wbi_helpers.py +++ b/test/test_wbi_helpers.py @@ -3,6 +3,7 @@ mapping), search, merge, SPARQL and the various pure helper functions. """ import logging +from types import MappingProxyType import pytest import requests @@ -144,6 +145,46 @@ def test_assert_user_and_bot_flags(self, wikibase, item_q582): assert wikibase.last_request['assert'] == 'bot' +class TestRequestDataNotMutated: + """The caller's request payload must be left untouched by the API helpers.""" + + def test_helper_does_not_mutate_caller_data(self, wikibase, item_q582): + login = FakeLogin(mediawiki_api_url=wikibase.mediawiki_api_url) + data = {'action': 'wbgetentities', 'ids': 'Q582'} + + mediawiki_api_call_helper(data=data, login=login, is_bot=True) + + assert data == {'action': 'wbgetentities', 'ids': 'Q582'} + # The copy sent over the wire still gets the automatic parameters + assert wikibase.last_request['token'] == login.edit_token + assert wikibase.last_request['assert'] == 'bot' + assert wikibase.last_request['maxlag'] == '5' + assert wikibase.last_request['format'] == 'json' + + def test_api_call_does_not_mutate_caller_data(self, requests_mock): + url = 'https://example.org/w/api.php' + requests_mock.post(url, json={'success': 1}) + data = {'action': 'query'} + + mediawiki_api_call('POST', mediawiki_api_url=url, data=data) + + assert data == {'action': 'query'} + assert 'format=json' in requests_mock.last_request.text + + def test_api_call_accepts_read_only_mapping(self, requests_mock): + url = 'https://example.org/w/api.php' + requests_mock.post(url, json={'success': 1}) + + assert mediawiki_api_call('POST', mediawiki_api_url=url, data=MappingProxyType({'action': 'query'})) == {'success': 1} + assert 'format=json' in requests_mock.last_request.text + + def test_helper_accepts_none_data(self, requests_mock): + url = 'https://example.org/w/api.php' + requests_mock.post(url, json={'success': 1}) + + assert mediawiki_api_call_helper(data=None, mediawiki_api_url=url, allow_anonymous=True) == {'success': 1} + + class TestUserAgent: def test_no_warning_when_user_agent_set(self, wikibase, item_q582, caplog): data = {'action': 'wbgetentities', 'ids': 'Q582', 'format': 'json'} diff --git a/wikibaseintegrator/wbi_helpers.py b/wikibaseintegrator/wbi_helpers.py index 2c5afc4e..e48b239d 100644 --- a/wikibaseintegrator/wbi_helpers.py +++ b/wikibaseintegrator/wbi_helpers.py @@ -166,7 +166,7 @@ def mediawiki_api_call_helper(data: dict[str, Any], login: _Login | None = None, """ mediawiki_api_url = str(mediawiki_api_url or config['MEDIAWIKI_API_URL']) user_agent = user_agent or (str(config['USER_AGENT']) if config['USER_AGENT'] is not None else None) - data = data.copy() + data = dict(data) if data is not None else None hostname = urlparse(mediawiki_api_url).hostname if hostname is not None and hostname.endswith(('wikidata.org', 'wikipedia.org', 'wikimedia.org')) and user_agent is None: From f600ad65be16e7fe439e8a6a7fa702001701cef8 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:58:49 +0200 Subject: [PATCH 3/3] Remove accidentally committed local files Co-Authored-By: Claude Opus 5 --- .run/pytest in Podman.run.xml | 42 ------------- TODO.md | 113 ---------------------------------- 2 files changed, 155 deletions(-) delete mode 100644 .run/pytest in Podman.run.xml delete mode 100644 TODO.md diff --git a/.run/pytest in Podman.run.xml b/.run/pytest in Podman.run.xml deleted file mode 100644 index 9b3d1494..00000000 --- a/.run/pytest in Podman.run.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a9f551d7..00000000 --- a/TODO.md +++ /dev/null @@ -1,113 +0,0 @@ -# TODO — Revue du projet (2026-07-27) - -Fichier local, non versionné (ajouté au .gitignore). Issu de la revue complète du 27 juillet 2026. -État au moment de la revue : 203 tests OK (1,5 s), couverture 81 %, pylint 8.98/10, 41 issues ouvertes, dernière release 0.12.15 (21 déc. 2025). - -## 1. Quick wins (à faire en premier) - -- [x] **Fixer les 2 erreurs mypy sur master** — `wbi_login.py:339` et `wbi_login.py:428` : - `session.headers.get()` renvoie `str | bytes`, incompatible avec `dict[str, str]` attendu par `_perform_login()`. - Introduites par le bump mypy 2.3.0 (#1006). *(fait le 2026-07-27, garde `isinstance(…, str)`)* -- [x] **Corriger les `paths` de `.github/workflows/python-lint.yaml`** — ajouter `pyproject.toml` et `poetry.lock` - (comme `python-pytest.yaml`), sinon les bumps de dépendances mergent sans passer le lint (cause racine du point précédent). - *(fait le 2026-07-27)* -- [x] **Ajouter le marqueur `py.typed`** (PEP 561) — fichier vide `wikibaseintegrator/py.typed` + déclaration dans - `pyproject.toml`. Sans lui, les type hints ne profitent pas aux utilisateurs en aval. - *(fait le 2026-07-27, présence dans la wheel vérifiée via `poetry build`)* -- [x] **README** : supprimer/corriger la phrase « But WikibaseIntegrator lack the "fastrun" functionality » - (contradictoire, `wbi_fastrun.py` existe). *(fait le 2026-07-27 — texte identique à celui de la PR #1001 pour éviter un conflit de merge)* -- [x] **AGENTS.md** : la version n'est plus « mirrored in `__init__.py` » (lecture via `importlib.metadata` désormais). - *(fait le 2026-07-27)* - -→ Reste à faire pour cette section : commit + push (non fait, en attente de validation). - -## 2. Écluser la file d'attente (axe n° 1) - -- [ ] **Merger les 5 PRs perso ouvertes** : - - ~~#1001 — fix fastrun (`fix-wbi-fastrun`)~~ → **à fermer au profit de #333** (voir ci-dessous) - - #983 — SPARQL en POST body - - #982 — non-mutation des données passées en argument - - #981 — supprimer les sessions requests module-level - - #980 — AnonymousEditError + meilleure gestion d'erreurs -- [ ] **Publier la release 0.12.16** — 7 mois depuis 0.12.15, master contient déjà : récupération de session (#999), - abandon de mwoauth (#998), `check_constraints()` (#997), refactor entités (#996). -- [x] **Décision fastrun (2026-07-28) : retenir #333 (`new-fastrun`), fermer #1001.** - Vérifié : #333 = 235 tests, corrige le crash `wbi_fastrun.py:291` que #1001 laisse intact, et interroge SPARQL - par statement au lieu de la requête unique sur tout le corpus (celle qui a enchaîné cinq 504 WDQS au benchmark). - Coût : rebase nécessaire (conflits), rupture d'API fastrun (`get_items` → `get_entities`), donc changelog. -- [x] **Porter le meilleur de #1001 dans #333** → branche `port-1001-into-new-fastrun` (poussée, 253 tests). - `BaseDataType.from_sparql_value()` générique + implémentations pour Property, Lexeme, Form, Sense, GeoShape, - TabularData, CommonsMedia. Lève la limitation « datatypes non comparables » et corrige un faux appariement - silencieux de CommonsMedia (stockait l'URL Commons entière). -- [x] **Rebaser `new-fastrun` sur master + intégrer le portage** (fait le 2026-07-28). - PR #333 est passée de CONFLICTING à **MERGEABLE** : 6 commits, 0 de retard, 253 tests, mypy/isort/codespell/flynt - clean (les 2 erreurs mypy héritées ont disparu avec #1010). Un seul conflit, sur la phrase fastrun du README - (master via #1012 vs `new-fastrun`) — résolu en gardant celle de `new-fastrun`, plus précise. - Sauvegarde d'avant rebase : branche locale `new-fastrun-bkp-20260728` (`195ee1d`). -- [x] **#1001 fermée** (2026-07-28) avec un commentaire renvoyant vers #333 et détaillant la raison - (architecture de la requête en masse, crash `:291`, et ce qui a été porté). Branche `fix-wbi-fastrun` conservée. -- [x] **Branche `port-1001-into-new-fastrun` supprimée** (locale + distante) après vérification que son commit - est intégré à l'identique dans #333. -- [ ] **Relire et merger #333** — dernière étape du chantier fastrun. - Ne pas oublier : rupture d'API fastrun (`get_items` → `get_entities`, `write_required` gagne - `entity_filter`/`property_filter`) → entrée de changelog + note de version. - Après merge : supprimer la sauvegarde locale `new-fastrun-bkp-20260728` si tout va bien. -- [ ] **Grooming du backlog** (41 issues, les plus vieilles de 2023) : fermer l'obsolète, étiqueter le reste. -- [ ] **Bugs utilisateurs à prioriser** : - - #953 — KeyError `'hash'` au parsing des références - - #883 — erreur upload statement avec qualifier Time - - #836 — impossible d'écrire dans un MediaInfo vide -- [ ] Traiter les PRs externes en attente : #917 (merge multi-items), #926 (draft, défaut `Claims.add()`), #834, #663 (draft). - -## 3. Robustesse réseau - -- [ ] **Retry bloquant** dans `wbi_helpers.mediawiki_api_call()` : `max_retries=100` × `sleep(60)` = jusqu'à ~100 min - de blocage silencieux. Réduire les défauts + backoff progressif (le décorateur `wbi_backoff` ne couvre que SPARQL/login). -- [ ] **Lookups de datatypes fragiles** : le motif `[x for x in BaseDataType.subclasses if x.DTYPE == ...][0]` - (models/claims.py:127, wbi_fastrun.py, wbi_helpers.py) lève une `IndexError` brute sur datatype inconnu. - → registre avec message d'erreur explicite listant les datatypes supportés. -- [ ] (Long terme, si demande) API async via httpx pour les bots de masse. - -## 4. Architecture / dette - -- [ ] **Boilerplate propriétés** : centaines de getters/setters triviaux (baseentity.py, claims.py…). - → dataclasses (cf. branche dormante `implement-pep557`) ou suppression des propriétés triviales. -- [ ] **Imports cycliques** (pylint R0401) : `wikibaseintegrator ↔ entities ↔ wbi_helpers ↔ wbi_login`, - masqués par des imports locaux dans les fonctions. -- [ ] **État global restant** : dict `config` mutable sans validation, cache module-level `properties_dt` - (format2wbi), liste globale `fastrun_store`. (#981/#982 traitent déjà les sessions et la mutation.) -- [ ] **`format2wbi` / `_json2datatype`** : expérimentaux, non testés, références non attachées. - → finir ou sortir du module principal. - -## 5. Tests - -- [ ] **`wbi_helpers.py` à 69 %** : la logique de retry (rate limiting, readonly, maxlag) est en `pragma: no cover` - alors qu'elle est testable offline avec requests-mock. Code critique pour un bot. -- [ ] Modules les moins couverts : monolingualtext (70 %), globecoordinate (72 %), baseentity (73 %), references (77 %). - -## 6. Outillage / packaging - -- [ ] **ruff** : remplacerait isort + flynt et une partie de pylint (une config, beaucoup plus rapide en CI). -- [ ] **pre-commit** : la branche `add-pre-commit` existe déjà, jamais mergée. -- [ ] **PEP 621** : migrer `[tool.poetry]` vers `[project]` (supporté par Poetry 2). -- [ ] **Couverture en CI + badge** (issue #895). -- [ ] Classifieurs `pyproject.toml` : `Python :: 3.15` déclaré alors que 3.15 n'est pas sorti ; - `Development Status :: 4 - Beta` ne reflète plus la maturité. - -## 7. Stratégique - -- [ ] **API REST Wikibase** (issue #518) : direction poussée par Wikimedia. Au minimum une exploration de design - (module `wbi_rest` parallèle plutôt que réécriture). -- [ ] **Ergonomie 0.13** (avec dépréciations propres) : - - défaut `REPLACE_ALL` de `Claims.add()` (piège connu, cf. #926 / #841) - - confusion `len(claims)` (nb propriétés) vs `count()` (nb claims) - - supprimer la classe dépréciée `BColors` -- [ ] **Docs** : README de 35 Ko monolithique → déplacer les guides vers Sphinx/RTD, garder un README court. - -## Ordre de priorité suggéré - -1. Section 1 (quick wins) — ~1 h de travail, débloque la CI. -2. Section 2 — merges + release 0.12.16. -3. Bugs utilisateurs (#953, #883, #836) + grooming. -4. Sections 3–6 au fil de l'eau. -5. Section 7 ensuite.