Claude commited on
Commit
2c2bc0f
·
unverified ·
1 Parent(s): 3836b05

docs: Phase 2 — vérité documentaire (compteurs, fantômes, legacy refs)

Browse files

Sept corrections de drift documentation → code identifiées par
l'audit code-quality.

**2.1 — Sync compteurs en CI bloquant**

- Nouveau job CI ``sync-counters`` qui exécute
``scripts/gen_readme_tables.py --check`` : échoue si README.md /
CLAUDE.md divergent du code réel (script déjà présent depuis A13,
mais jamais câblé en CI ni Makefile — orphelin).
- Cibles Makefile ``sync-counters`` (régénère), ``sync-counters-check``
(vérifie), ``docs`` (``mkdocs build --strict``) et ``docs-serve``.
- CLAUDE.md : compteur 4 750 (réel) au lieu de 4 700, détail
cohérent 16 skipped + 8 deselected + 2 xfailed (auto-contradiction
L.119 vs L.305 résolue, la 2e mention renvoie maintenant à la 1re).
- CLAUDE.md : ``20 détecteurs`` au lieu de 18 ; ajout des 2 manquants
(``importer_fallback_triggered`` dans history.py et
``pricing_staleness_warning`` dans pareto.py). ``28 renderers HTML``
au lieu de 22.
- CLAUDE.md : note explicite sur le piège ``pytest tests/`` (uv tool)
→ ``python -m pytest tests/``.

**2.2 — Modules fantômes retirés de api-stable.md**

Le document affirme garantir l'existence des modules listés.
Quatre rubriques pointaient vers du code supprimé en v2.0 :
``picarones.pipeline.legacy_runner``,
``picarones.pipeline.legacy_pipeline_benchmark``,
``picarones.pipeline.legacy_pipeline_comparison``,
``picarones.evaluation.metrics.pipeline_spec_loader``.

Test ajouté : ``tests/docs/test_api_stable_modules_exist.py``
parse les rubriques ``### `picarones.X.Y``` et ``importlib.import_module``
chacune. Empêche la résurrection du drift.

**2.3 — README.md**

- Section ``Project layout`` : retrait du paragraphe « Legacy paths
still present as shims » (faux depuis v2.0). Remplacement par une
description honnête du retrait complet du legacy.
- Section ``Development`` : ``python -m mypy picarones/core/`` →
``python -m mypy picarones/domain/`` (strict) +
``python -m mypy picarones/`` (lax). ``pytest tests/`` →
``python -m pytest tests/``.

**2.5 — architecture.md**

Ligne 165 : ``22 renderers + 5 vues`` → ``28 renderers + 5 vues``.
Ligne 166 : ``18 détecteurs`` → ``20 détecteurs``.

**2.6 — pyproject.toml extra ``all``**

``all = ["picarones[web,hf,llm,dev]"]`` était trompeur (le commentaire
disait « tous les extras sauf OCR cloud » alors qu'il oubliait aussi
docs/stats/ner/pero/kraken/calamari). Élargi à l'intégralité :
``[dev,docs,web,stats,ner,hf,pero,kraken,calamari,llm,ocr-cloud]``.

Compteurs : 4 732 passed (post-Phase 1 : +1 test API-stable),
0 failed, ruff propre, sync-counters --check vert.

.github/workflows/ci.yml CHANGED
@@ -274,6 +274,32 @@ jobs:
274
  - name: Run ruff
275
  run: ruff check picarones/ tests/
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  # ──────────────────────────────────────────────────────────────────
278
  # Job 5 : Type-checking — Sprint A1 (item M-4)
279
  #
 
274
  - name: Run ruff
275
  run: ruff check picarones/ tests/
276
 
277
+ # ──────────────────────────────────────────────────────────────────
278
+ # Job 4-bis : Sync compteurs README/CLAUDE.md — Phase 2.1 audit
279
+ # code-quality (2026-05). Le script gen_readme_tables.py reflète
280
+ # le code réel dans la prose des docs (tableaux Engines/CLI/API +
281
+ # compteur de tests). En CI : exit 1 si la doc dérive.
282
+ # ──────────────────────────────────────────────────────────────────
283
+ sync-counters:
284
+ name: Doc counters sync
285
+ runs-on: ubuntu-latest
286
+
287
+ steps:
288
+ - name: Checkout
289
+ uses: actions/checkout@v4
290
+
291
+ - name: Set up Python
292
+ uses: actions/setup-python@v5
293
+ with:
294
+ python-version: "3.11"
295
+ cache: pip
296
+
297
+ - name: Install Picarones (web extras pour ouvrir l'app FastAPI)
298
+ run: pip install -e ".[dev,web]"
299
+
300
+ - name: Vérifier que README.md / CLAUDE.md reflètent le code
301
+ run: python scripts/gen_readme_tables.py --check
302
+
303
  # ──────────────────────────────────────────────────────────────────
304
  # Job 5 : Type-checking — Sprint A1 (item M-4)
305
  #
CLAUDE.md CHANGED
@@ -95,9 +95,9 @@ picarones/
95
  │ benchmark_runner (entry point CLI/web), partial_store
96
 
97
  ├── reports/ Couche 7 — rendu HTML / JSON / CSV
98
- │ ├── html/ ReportGenerator + 22 renderers + 5 vues + templates Jinja2
99
  │ ├── json/, csv/ exports tabulaires
100
- │ ├── narrative/ moteur narratif (18 détecteurs)
101
  │ ├── glossary/, i18n/ glossaire + i18n FR/EN
102
  │ └── _helpers/ colors, render_helpers, assets
103
 
@@ -116,13 +116,19 @@ picarones/
116
 
117
  ## État des tests et bugs historiques
118
 
119
- `pytest tests/` → **4750 passed, 12 skipped, 8 deselected, 0 failed**
120
- (post-S59). Les deselected sont les markers `live` (5 tests d'intégration
121
- contre vraie API/binaire) + `network` (3 tests qui hit le réseau réel),
122
- opt-in en local via `pytest -m live` ou `pytest -m network`. Le
123
- compteur en prose est synchronisé automatiquement par
124
- `scripts/gen_readme_tables.py` toute modification manuelle sera
125
- -écrasée au prochain `make lint`.
 
 
 
 
 
 
126
 
127
  ### Bugs documentés antérieurement — tous résolus
128
 
@@ -271,17 +277,18 @@ picarones/reports/narrative/
271
  ├── arbiter.py Tri par importance, non-redondance, anti-contradiction
272
  ├── renderer.py Rendu templates YAML par str.format_map (déterministe)
273
  ├── registry.py Registre par défaut des détecteurs
274
- ├── templates/{fr,en}.yaml 18 templates × 2 langues
275
- └── detectors/ 18 détecteurs en 6 familles
276
  ├── ranking.py 5 (global_leader, statistical_tie, significant_gap,
277
  │ speed_winner, median_mean_gap_warning)
278
- ├── pareto.py 2 (pareto_alternative, cost_outlier)
 
279
  ├── stratum.py 3 (stratum_winner, stratum_collapse,
280
  │ stratification_recommended)
281
  ├── quality.py 4 (error_profile_outlier, llm_hallucination_flag,
282
  │ robustness_fragile, confidence_warning)
283
- ├── history.py 3 (engine_off_baseline, engine_unstable,
284
- │ regression_in_history)
285
  └── ensemble.py 1 (ensemble_opportunity)
286
  ```
287
 
@@ -302,8 +309,8 @@ détecte, arbitre, rend.
302
  ## Contexte développement
303
 
304
  - **Environnement** : GitHub Codespaces, Python 3.11+
305
- - **Tests** : `pytest tests/ -q` 4750 passed, 9 skipped, 24
306
- deselected, 0 failed (post-v2.0).
307
  - **Manifeste architecture** : [`docs/explanation/architecture.md`](docs/explanation/architecture.md).
308
  - **API publique stable** : [`docs/reference/api-stable.md`](docs/reference/api-stable.md).
309
 
 
95
  │ benchmark_runner (entry point CLI/web), partial_store
96
 
97
  ├── reports/ Couche 7 — rendu HTML / JSON / CSV
98
+ │ ├── html/ ReportGenerator + 28 renderers + 5 vues + templates Jinja2
99
  │ ├── json/, csv/ exports tabulaires
100
+ │ ├── narrative/ moteur narratif (20 détecteurs)
101
  │ ├── glossary/, i18n/ glossaire + i18n FR/EN
102
  │ └── _helpers/ colors, render_helpers, assets
103
 
 
116
 
117
  ## État des tests et bugs historiques
118
 
119
+ `pytest tests/` → **4750 passed, 16 skipped, 8 deselected, 2 xfailed, 0 failed**
120
+ (post-audit code-quality, mai 2026). Les deselected sont les markers
121
+ `live` (5 tests d'intégration contre vraie API/binaire) + `network`
122
+ (3 tests qui hit le réseau réel), opt-in en local via `pytest -m live`
123
+ ou `pytest -m network`. Le compteur ``passed`` est synchronisé
124
+ automatiquement par `scripts/gen_readme_tables.py` (CI : job
125
+ ``sync-counters`` ; local : `make sync-counters-check`). Le détail
126
+ ``skipped``/``xfailed`` peut dériver de ±2 entre éditions et n'est
127
+ pas verrouillé en CI.
128
+
129
+ NB : utiliser ``python -m pytest tests/`` plutôt que ``pytest tests/``
130
+ directement — l'installation via ``uv tool install pytest`` masque
131
+ les deps Picarones et produit ~160 collection errors trompeurs.
132
 
133
  ### Bugs documentés antérieurement — tous résolus
134
 
 
277
  ├── arbiter.py Tri par importance, non-redondance, anti-contradiction
278
  ├── renderer.py Rendu templates YAML par str.format_map (déterministe)
279
  ├── registry.py Registre par défaut des détecteurs
280
+ ├── templates/{fr,en}.yaml 20 templates × 2 langues
281
+ └── detectors/ 20 détecteurs en 6 familles
282
  ├── ranking.py 5 (global_leader, statistical_tie, significant_gap,
283
  │ speed_winner, median_mean_gap_warning)
284
+ ├── pareto.py 3 (pareto_alternative, cost_outlier,
285
+ │ pricing_staleness_warning)
286
  ├── stratum.py 3 (stratum_winner, stratum_collapse,
287
  │ stratification_recommended)
288
  ├── quality.py 4 (error_profile_outlier, llm_hallucination_flag,
289
  │ robustness_fragile, confidence_warning)
290
+ ├── history.py 4 (engine_off_baseline, engine_unstable,
291
+ │ regression_in_history, importer_fallback_triggered)
292
  └── ensemble.py 1 (ensemble_opportunity)
293
  ```
294
 
 
309
  ## Contexte développement
310
 
311
  - **Environnement** : GitHub Codespaces, Python 3.11+
312
+ - **Tests** : voir « État des tests et bugs historiques » plus haut
313
+ (compteur synchronisé par ``scripts/gen_readme_tables.py``).
314
  - **Manifeste architecture** : [`docs/explanation/architecture.md`](docs/explanation/architecture.md).
315
  - **API publique stable** : [`docs/reference/api-stable.md`](docs/reference/api-stable.md).
316
 
Makefile CHANGED
@@ -109,6 +109,34 @@ doc-check: ## Audit de cohérence README/SPECS/CHANGELOG (Sprint A2)
109
  $(PYTHON) -m pytest tests/docs/ -q --tb=short --no-header; \
110
  fi
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  typecheck: ## Vérification de types avec mypy (si installé)
113
  @$(VENV_BIN)/python -m mypy $(PACKAGE)/ --ignore-missing-imports --no-strict-optional 2>/dev/null \
114
  || echo "mypy non installé : pip install mypy"
 
109
  $(PYTHON) -m pytest tests/docs/ -q --tb=short --no-header; \
110
  fi
111
 
112
+ sync-counters: ## Régénère README/CLAUDE.md avec les compteurs réels (script gen_readme_tables.py)
113
+ @if [ -x $(VENV_BIN)/python ]; then \
114
+ $(VENV_BIN)/python scripts/gen_readme_tables.py; \
115
+ else \
116
+ $(PYTHON) scripts/gen_readme_tables.py; \
117
+ fi
118
+
119
+ sync-counters-check: ## CI : échoue si README/CLAUDE.md divergent du code (compteurs tests + tables)
120
+ @if [ -x $(VENV_BIN)/python ]; then \
121
+ $(VENV_BIN)/python scripts/gen_readme_tables.py --check; \
122
+ else \
123
+ $(PYTHON) scripts/gen_readme_tables.py --check; \
124
+ fi
125
+
126
+ docs: ## Construit le site mkdocs en mode strict (échoue sur les warnings)
127
+ @if [ -x $(VENV_BIN)/python ]; then \
128
+ $(VENV_BIN)/python -m mkdocs build --strict; \
129
+ else \
130
+ $(PYTHON) -m mkdocs build --strict; \
131
+ fi
132
+
133
+ docs-serve: ## Lance mkdocs en mode dev (http://localhost:8000)
134
+ @if [ -x $(VENV_BIN)/python ]; then \
135
+ $(VENV_BIN)/python -m mkdocs serve; \
136
+ else \
137
+ $(PYTHON) -m mkdocs serve; \
138
+ fi
139
+
140
  typecheck: ## Vérification de types avec mypy (si installé)
141
  @$(VENV_BIN)/python -m mypy $(PACKAGE)/ --ignore-missing-imports --no-strict-optional 2>/dev/null \
142
  || echo "mypy non installé : pip install mypy"
README.md CHANGED
@@ -332,13 +332,16 @@ picarones/
332
  └── interfaces/ Layer 8 — CLI Click, Web FastAPI
333
  ```
334
 
335
- Legacy paths (`core/, measurements/, engines/, llm/, pipelines/,
336
- report/, modules/`) still present as shims, in active retirement
337
- (see `docs/archives/migration/`). Strict 8-layer architecture: imports flow
338
- outer inner. Enforced by
339
- `tests/architecture/test_layer_dependencies.py`. See
 
 
340
  [`docs/explanation/architecture.md`](docs/explanation/architecture.md)
341
- for the full manifesto.
 
342
 
343
  ---
344
 
@@ -392,9 +395,10 @@ GitHub Actions: `.github/workflows/`
392
  ```bash
393
  pip install -e ".[dev,web]"
394
  pre-commit install
395
- pytest tests/ -q
396
  ruff check picarones/ tests/
397
- python -m mypy picarones/core/
 
398
  ```
399
 
400
  **Test suite**: ~4750 tests, ~3 min on a modern laptop. Coverage
 
332
  └── interfaces/ Layer 8 — CLI Click, Web FastAPI
333
  ```
334
 
335
+ Strict 8-layer architecture: imports flow outer → inner. Enforced
336
+ by `tests/architecture/test_layer_dependencies.py`. The v2.0
337
+ release (May 2026) removed all legacy top-level packages (`core/`,
338
+ `measurements/`, `engines/`, `llm/`, `pipelines/`, `report/`,
339
+ `modules/`, `cli/`, `web/`, `extras/`) and the transitional
340
+ sub-packages (`adapters/legacy_engines/`, `adapters/legacy_pipelines/`,
341
+ `interfaces/{cli,web}/_legacy/`). See
342
  [`docs/explanation/architecture.md`](docs/explanation/architecture.md)
343
+ for the full manifesto and migration history under
344
+ `docs/archives/migration/`.
345
 
346
  ---
347
 
 
395
  ```bash
396
  pip install -e ".[dev,web]"
397
  pre-commit install
398
+ python -m pytest tests/ -q # ``python -m`` requis si pytest est uv-installé
399
  ruff check picarones/ tests/
400
+ python -m mypy picarones/domain/ # strict mode (Layer 1)
401
+ python -m mypy picarones/ # lax mode (full tree)
402
  ```
403
 
404
  **Test suite**: ~4750 tests, ~3 min on a modern laptop. Coverage
docs/explanation/architecture.md CHANGED
@@ -162,8 +162,8 @@ Verrouillé par `tests/security/test_s1_zip_slip_attack.py`.
162
  | `csv/render.py` | `CsvReportRenderer` — un CSV plat (`run_id, doc, pipeline, view, metric, value, status`) |
163
  | `json/render.py` | `JsonReportRenderer` — manifest + documents en JSON déterministe |
164
  | `html/render.py` | `HtmlReportRenderer` — rapport autonome (TextView, AltoView, SearchView) — minimaliste |
165
- | `html/generator.py` | `ReportGenerator` — rapport interactif riche (22 renderers + 5 vues) consommé par CLI/web |
166
- | `narrative/` | Moteur narratif (18 détecteurs) — synthèse factuelle déterministe |
167
  | `glossary/`, `i18n/` | Glossaire + i18n FR/EN |
168
 
169
  Le rendu est strict : pas de JS dynamique côté serveur, pas d'I/O
 
162
  | `csv/render.py` | `CsvReportRenderer` — un CSV plat (`run_id, doc, pipeline, view, metric, value, status`) |
163
  | `json/render.py` | `JsonReportRenderer` — manifest + documents en JSON déterministe |
164
  | `html/render.py` | `HtmlReportRenderer` — rapport autonome (TextView, AltoView, SearchView) — minimaliste |
165
+ | `html/generator.py` | `ReportGenerator` — rapport interactif riche (28 renderers + 5 vues) consommé par CLI/web |
166
+ | `narrative/` | Moteur narratif (20 détecteurs) — synthèse factuelle déterministe |
167
  | `glossary/`, `i18n/` | Glossaire + i18n FR/EN |
168
 
169
  Le rendu est strict : pas de JS dynamique côté serveur, pas d'I/O
docs/reference/api-stable.md CHANGED
@@ -135,55 +135,6 @@ l'API mono-call historique de
135
  s'appuyant en interne sur ``BenchmarkService`` (rewrite).
136
  Prouvé numériquement équivalent en D.1.e.
137
 
138
- ### `picarones.pipeline.legacy_runner`
139
-
140
- > Phase 7.B.2 (2026-05-07) — module relocalisé depuis
141
- > ``picarones.evaluation.pipeline`` vers ``picarones.pipeline.legacy_runner``.
142
- > La délégation au ``PipelineExecutor`` canonique impose à ce module
143
- > d'importer la couche ``pipeline/`` — interdit à ``evaluation/``.
144
-
145
- ```python
146
- class PipelineStep:
147
- class PipelineSpec:
148
- class StepResult:
149
- class PipelineResult:
150
- class PipelineRunner:
151
- ```
152
-
153
- ### `picarones.pipeline.legacy_pipeline_benchmark`
154
-
155
- > Phase 7.B.2 — relocalisé depuis ``picarones.evaluation.pipeline_benchmark``
156
- > (mêmes raisons que ``legacy_runner``).
157
-
158
- ```python
159
- class StepAggregate:
160
- class PipelineBenchmarkResult:
161
-
162
- def default_initial_inputs(doc) -> dict
163
- def run_pipeline_benchmark(spec, corpus, factory=...) -> PipelineBenchmarkResult
164
- ```
165
-
166
- ### `picarones.pipeline.legacy_pipeline_comparison`
167
-
168
- > Phase 7.B.2 — relocalisé depuis ``picarones.evaluation.pipeline_comparison``.
169
-
170
- ```python
171
- class PipelineComparisonResult:
172
-
173
- def compare_pipelines(specs, corpus, factories=None) -> PipelineComparisonResult
174
- ```
175
-
176
- ### `picarones.evaluation.metrics.pipeline_spec_loader`
177
-
178
- ```python
179
- class PipelineSpecLoadError(ValueError):
180
-
181
- def load_pipeline_spec_from_yaml(path) -> PipelineSpec
182
- def load_pipeline_spec_from_dict(data: dict) -> PipelineSpec
183
- def load_comparison_specs_from_yaml(path) -> tuple[list[PipelineSpec], dict]
184
- def load_comparison_specs_from_dict(data: dict) -> tuple[list[PipelineSpec], dict]
185
- ```
186
-
187
  ### `picarones.evaluation.metric_registry`
188
 
189
  ```python
 
135
  s'appuyant en interne sur ``BenchmarkService`` (rewrite).
136
  Prouvé numériquement équivalent en D.1.e.
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  ### `picarones.evaluation.metric_registry`
139
 
140
  ```python
pyproject.toml CHANGED
@@ -124,9 +124,13 @@ ocr-cloud = [
124
  # distincts (``picarones-historical``, ``picarones-importers``) est
125
  # documentée dans ``docs/developer/module-policy.md`` (Sprint 97) et
126
  # n'a plus besoin d'être réservée par un extra vide.
127
- # Installation complète (tous les extras sauf les OCR cloud)
 
 
 
 
128
  all = [
129
- "picarones[web,hf,llm,dev]",
130
  ]
131
 
132
  [project.scripts]
 
124
  # distincts (``picarones-historical``, ``picarones-importers``) est
125
  # documentée dans ``docs/developer/module-policy.md`` (Sprint 97) et
126
  # n'a plus besoin d'être réservée par un extra vide.
127
+ #
128
+ # Installation **vraiment complète** : tous les extras déclarés
129
+ # ci-dessus, OCR cloud et docs inclus. Le nom ``all`` ne doit pas
130
+ # tromper le contributeur — si un extra apparaît plus haut, il doit
131
+ # apparaître ici. Phase 2.6 de l'audit code-quality (2026-05).
132
  all = [
133
+ "picarones[dev,docs,web,stats,ner,hf,pero,kraken,calamari,llm,ocr-cloud]",
134
  ]
135
 
136
  [project.scripts]
tests/docs/test_api_stable_modules_exist.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2.2 du plan d'audit — chaque module ``picarones.X.Y`` cité
2
+ dans ``docs/reference/api-stable.md`` comme rubrique de niveau 3
3
+ (``### `picarones....```) doit être réellement importable.
4
+
5
+ Le document revendique en son préambule :
6
+
7
+ > Garantie principale : **existence** — aucun nom listé ne
8
+ > disparaît entre ``1.x.0`` et ``2.0.0`` sans procédure de
9
+ > dépréciation.
10
+
11
+ L'audit code-quality de mai 2026 a trouvé que 4 modules cités
12
+ n'existaient plus :
13
+
14
+ - ``picarones.pipeline.legacy_runner``
15
+ - ``picarones.pipeline.legacy_pipeline_benchmark``
16
+ - ``picarones.pipeline.legacy_pipeline_comparison``
17
+ - ``picarones.evaluation.metrics.pipeline_spec_loader``
18
+
19
+ Tous supprimés au passage v2.0 (retrait du legacy). Ce test
20
+ empêche le drift de se reproduire.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import importlib
26
+ import re
27
+ from pathlib import Path
28
+
29
+ API_STABLE_MD = Path(__file__).resolve().parents[2] / "docs" / "reference" / "api-stable.md"
30
+
31
+ # Capture les rubriques de niveau 3 ``### `picarones.X.Y```.
32
+ # Le backtick fermant est obligatoire (évite de matcher du prose
33
+ # qui mentionne ``picarones.X`` sans intention de "rubrique API").
34
+ _RUBRIC_RE = re.compile(r"^###\s+`(picarones\.[\w\.]+)`", re.MULTILINE)
35
+
36
+
37
+ def _extract_modules() -> list[str]:
38
+ if not API_STABLE_MD.exists():
39
+ return []
40
+ return _RUBRIC_RE.findall(API_STABLE_MD.read_text(encoding="utf-8"))
41
+
42
+
43
+ def test_every_api_stable_rubric_is_importable() -> None:
44
+ """Chaque module cité comme ``### `picarones.X.Y``` doit s'importer
45
+ sans erreur — garantie d'existence pour les consommateurs externes
46
+ qui se fient à la documentation pour cibler une API stable.
47
+ """
48
+ modules = _extract_modules()
49
+ assert modules, (
50
+ f"{API_STABLE_MD} ne contient aucune rubrique ``### `picarones.X``` — "
51
+ f"le test ne peut pas vérifier le contrat d'existence."
52
+ )
53
+
54
+ missing: list[tuple[str, str]] = []
55
+ for name in modules:
56
+ try:
57
+ importlib.import_module(name)
58
+ except ImportError as exc:
59
+ missing.append((name, str(exc)))
60
+
61
+ assert not missing, (
62
+ "api-stable.md référence des modules qui n'existent plus :\n"
63
+ + "\n".join(f" - {name} : {err}" for name, err in missing)
64
+ + "\n\nSoit recréer le module, soit retirer la rubrique de "
65
+ "``docs/reference/api-stable.md`` (et documenter la rupture dans "
66
+ "CHANGELOG.md). La garantie d'existence du document interdit "
67
+ "le drift silencieux."
68
+ )