Spaces:
Sleeping
docs(index): repair broken links + lock against drift
Browse filesdocs/index.md est l'index canonique de la documentation, référencé
depuis le README et utilisé comme première porte d'entrée pour les
nouveaux contributeurs. Il contenait 5 liens cassés survivants du
rewrite, aucun test ne les détectait :
- tutorials/first-benchmark.md (absent)
- tutorials/writing-a-pipeline-module.md (absent)
- user/writing-a-pipeline-module.md (dossier user/ absent)
- developer/narrative-engine.md(.en.md) (existe seulement dans explanation/)
- migration/rewrite-status-s46.md (en réalité dans archives/migration/)
Plus 3 affirmations fausses dans la section Conventions :
- "reports_v2" (renommé "reports" en Sprint H.3)
- "L'arbo legacy reste exécutable" (supprimée à v2.0)
- "baseline 73, doit décroître" (vrai baseline = 164)
Réparations :
- Création de docs/tutorials/first-benchmark.md (tutoriel d'entrée,
~110 lignes, agrège install + demo + premier benchmark + interface
web) qui était référencé par index.md depuis longtemps mais
n'existait pas.
- Création de docs/tutorials/writing-a-pipeline-module.md (tutoriel
par l'exemple) qui pointe vers developer/module-policy.md pour
les détails normatifs.
- Corrections des liens narrative-engine vers explanation/ (où ils
existent réellement).
- Correction du lien rewrite-status-s46.md vers archives/migration/.
- Réécriture de la section Conventions pour refléter v2.0.
Nouveau test tests/docs/test_index_links_resolve.py qui parse tous
les liens markdown internes de docs/index.md et vérifie qu'ils
résolvent vers un fichier ou dossier réel. Empêche structurellement
le retour de cette classe de mensonge silencieux.
- docs/index.md +17 -14
- docs/tutorials/first-benchmark.md +134 -0
- docs/tutorials/writing-a-pipeline-module.md +150 -0
- tests/docs/test_index_links_resolve.py +100 -0
|
@@ -50,8 +50,8 @@ Vous ajoutez un adapter, une vue, une métrique, un détecteur narratif.
|
|
| 50 |
4. Étendre un sous-système :
|
| 51 |
[glossaire](developer/extending-glossary.md) ([EN](developer/extending-glossary.en.md)) ·
|
| 52 |
[i18n](developer/extending-i18n.md) ([EN](developer/extending-i18n.en.md)) ·
|
| 53 |
-
[moteur narratif](
|
| 54 |
-
5. Écrire un module pour le banc d'essai : [`
|
| 55 |
|
| 56 |
### …un mainteneur ou auditeur de sécurité
|
| 57 |
|
|
@@ -63,7 +63,7 @@ Vous évaluez Picarones avant un déploiement, un audit, une revue.
|
|
| 63 |
3. Threat model STRIDE : [`security/threat-model.md`](security/threat-model.md)
|
| 64 |
4. API publique stable et politique de versioning : [`reference/api-stable.md`](reference/api-stable.md)
|
| 65 |
5. Audits historiques : [`audits/`](audits/)
|
| 66 |
-
6. État du rewrite et migration : [`migration/rewrite-status-s46.md`](migration/rewrite-status-s46.md)
|
| 67 |
7. Reproductibilité bit-for-bit : [`reference/reproducibility-snapshots.md`](reference/reproducibility-snapshots.md)
|
| 68 |
|
| 69 |
### …un Délégué à la Protection des Données (DPO)
|
|
@@ -146,15 +146,18 @@ Vous évaluez les implications RGPD avant signature.
|
|
| 146 |
|
| 147 |
## Conventions
|
| 148 |
|
| 149 |
-
- **Une seule arborescence canonique
|
| 150 |
-
`domain → formats → evaluation → pipeline → adapters → app →
|
| 151 |
-
|
| 152 |
-
reste exécutable mais n'accepte plus de nouveau code.
|
| 153 |
- **Tout chemin `picarones/.../X.py` cité dans la doc doit exister**.
|
| 154 |
-
Vérifié par `tests/architecture/test_doc_paths.py` (
|
| 155 |
-
|
| 156 |
-
- **Les
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
4. Étendre un sous-système :
|
| 51 |
[glossaire](developer/extending-glossary.md) ([EN](developer/extending-glossary.en.md)) ·
|
| 52 |
[i18n](developer/extending-i18n.md) ([EN](developer/extending-i18n.en.md)) ·
|
| 53 |
+
[moteur narratif](explanation/narrative-engine.md) ([EN](explanation/narrative-engine.en.md))
|
| 54 |
+
5. Écrire un module pour le banc d'essai : [`tutorials/writing-a-pipeline-module.md`](tutorials/writing-a-pipeline-module.md)
|
| 55 |
|
| 56 |
### …un mainteneur ou auditeur de sécurité
|
| 57 |
|
|
|
|
| 63 |
3. Threat model STRIDE : [`security/threat-model.md`](security/threat-model.md)
|
| 64 |
4. API publique stable et politique de versioning : [`reference/api-stable.md`](reference/api-stable.md)
|
| 65 |
5. Audits historiques : [`audits/`](audits/)
|
| 66 |
+
6. État du rewrite et migration : [`archives/migration/rewrite-status-s46.md`](archives/migration/rewrite-status-s46.md)
|
| 67 |
7. Reproductibilité bit-for-bit : [`reference/reproducibility-snapshots.md`](reference/reproducibility-snapshots.md)
|
| 68 |
|
| 69 |
### …un Délégué à la Protection des Données (DPO)
|
|
|
|
| 146 |
|
| 147 |
## Conventions
|
| 148 |
|
| 149 |
+
- **Une seule arborescence canonique (v2.0)** :
|
| 150 |
+
`domain → formats → evaluation → pipeline → adapters → app → reports → interfaces`.
|
| 151 |
+
Les paquets legacy ont été supprimés en mai 2026.
|
|
|
|
| 152 |
- **Tout chemin `picarones/.../X.py` cité dans la doc doit exister**.
|
| 153 |
+
Vérifié par `tests/architecture/test_doc_paths.py` (ratchet
|
| 154 |
+
strictement décroissant).
|
| 155 |
+
- **Les tableaux générés** (engines, CLI, endpoints) sont régénérés
|
| 156 |
+
par `scripts/gen_readme_tables.py` — modifier le code, pas la doc.
|
| 157 |
+
Les compteurs en prose (nombre de tests, etc.) utilisent la
|
| 158 |
+
formulation approximative `N+ tests` pour absorber la dérive
|
| 159 |
+
OS-dépendante ; le chiffre exact vit dans le badge CI.
|
| 160 |
+
- **Cohérence FR/EN** : la langue canonique est le français. Une
|
| 161 |
+
surface EN réduite est listée dans
|
| 162 |
+
`tests/docs/test_translation_parity.py::TRANSLATION_PAIRS` —
|
| 163 |
+
toute paire FR/EN doit y figurer.
|
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Premier benchmark Picarones
|
| 2 |
+
|
| 3 |
+
Ce tutoriel guide un nouvel utilisateur — chercheur, archiviste,
|
| 4 |
+
conservateur — à travers son **premier benchmark OCR** complet, de
|
| 5 |
+
l'installation jusqu'à la lecture du rapport produit. Comptez 15
|
| 6 |
+
minutes pour la première fois, 2 minutes une fois familier.
|
| 7 |
+
|
| 8 |
+
> **Pré-requis** : Python 3.11+ et `pip`. Sur Linux, le binaire
|
| 9 |
+
> `tesseract` est nécessaire pour le moteur OCR par défaut
|
| 10 |
+
> (`apt-get install tesseract-ocr tesseract-ocr-fra` sur Debian/Ubuntu).
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 1. Installation
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
pip install -e ".[dev,web]"
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
L'extra `dev` apporte la suite de tests, `web` apporte l'interface
|
| 21 |
+
FastAPI (utile dès la deuxième session). Pour une installation
|
| 22 |
+
minimale en production, voir [`how-to/install.md`](../how-to/install.md).
|
| 23 |
+
|
| 24 |
+
Vérifiez :
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
picarones info
|
| 28 |
+
picarones engines
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Si `picarones engines` liste au moins `tesseract`, vous êtes prêt.
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 2. Générer un rapport de démonstration
|
| 36 |
+
|
| 37 |
+
Le mode `demo` produit un rapport HTML synthétique sans aucun moteur
|
| 38 |
+
installé. C'est le moyen le plus rapide de voir ce que Picarones
|
| 39 |
+
produit.
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
picarones demo --output rapport_demo.html
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
Ouvrez `rapport_demo.html` dans un navigateur. Vous obtenez un
|
| 46 |
+
rapport complet avec :
|
| 47 |
+
|
| 48 |
+
- agrégat CER/WER global ;
|
| 49 |
+
- diff caractère à caractère sur les documents ;
|
| 50 |
+
- diagramme CD (Critical Difference) si plus de 2 moteurs ;
|
| 51 |
+
- moteur narratif qui résume les faits saillants en prose.
|
| 52 |
+
|
| 53 |
+
Voir [`reading-a-report.md`](reading-a-report.md) pour la lecture
|
| 54 |
+
détaillée.
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## 3. Benchmark sur un vrai corpus
|
| 59 |
+
|
| 60 |
+
Préparez un dossier `mon_corpus/` qui contient :
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
mon_corpus/
|
| 64 |
+
├── doc1.jpg
|
| 65 |
+
├── doc1.gt.txt # transcription de référence
|
| 66 |
+
├── doc2.jpg
|
| 67 |
+
└── doc2.gt.txt
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Le format des transcriptions de référence est documenté dans
|
| 71 |
+
[`reference/normalization-profiles.md`](../reference/normalization-profiles.md).
|
| 72 |
+
|
| 73 |
+
Lancez le benchmark :
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
picarones run \
|
| 77 |
+
--corpus mon_corpus/ \
|
| 78 |
+
--engines tesseract \
|
| 79 |
+
--output rapport.html \
|
| 80 |
+
--json rapport.json
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
`rapport.html` contient le rendu visuel ; `rapport.json` contient
|
| 84 |
+
l'agrégat machine-lisible (utile pour CI ou comparaisons
|
| 85 |
+
longitudinales — voir
|
| 86 |
+
[`reference/reproducibility-snapshots.md`](../reference/reproducibility-snapshots.md)).
|
| 87 |
+
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
## 4. Comparer plusieurs moteurs
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
picarones run \
|
| 94 |
+
--corpus mon_corpus/ \
|
| 95 |
+
--engines tesseract,pero_ocr,mistral_ocr \
|
| 96 |
+
--output comparaison.html
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Le rapport affiche désormais :
|
| 100 |
+
|
| 101 |
+
- une ligne par moteur avec CER moyen + IC95 ;
|
| 102 |
+
- le diagramme CD (qui domine statistiquement qui) ;
|
| 103 |
+
- les diffs côte à côte ;
|
| 104 |
+
- les coûts (si moteurs cloud).
|
| 105 |
+
|
| 106 |
+
Le moteur narratif énonce les écarts significatifs, ne désigne
|
| 107 |
+
jamais un « gagnant ».
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## 5. Interface web (optionnelle)
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
picarones serve --port 7860
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Ouvre `http://localhost:7860`. L'interface permet d'upload un ZIP
|
| 118 |
+
de corpus et de lancer un benchmark interactif. Pour le déploiement
|
| 119 |
+
institutionnel, voir
|
| 120 |
+
[`operations/deployment-institutional.md`](../operations/deployment-institutional.md).
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
## Étapes suivantes
|
| 125 |
+
|
| 126 |
+
- Comprendre les métriques :
|
| 127 |
+
[`reference/views.md`](../reference/views.md),
|
| 128 |
+
[`reference/normalization-profiles.md`](../reference/normalization-profiles.md)
|
| 129 |
+
- Lire un rapport en détail :
|
| 130 |
+
[`reading-a-report.md`](reading-a-report.md)
|
| 131 |
+
- Écrire un module pour la pipeline :
|
| 132 |
+
[`writing-a-pipeline-module.md`](writing-a-pipeline-module.md)
|
| 133 |
+
- Étudier des cas d'usage :
|
| 134 |
+
[`case-studies/`](../case-studies/)
|
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Écrire un module pour le banc d'essai
|
| 2 |
+
|
| 3 |
+
Ce tutoriel montre **par l'exemple** comment écrire un module
|
| 4 |
+
Picarones qui peut être chargé dans une pipeline composée, audité,
|
| 5 |
+
et inclus dans un rapport. Pour la **politique normative complète**
|
| 6 |
+
(contrat d'interface, métadonnées obligatoires, règles d'audit),
|
| 7 |
+
voir [`developer/module-policy.md`](../developer/module-policy.md).
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Cas d'usage
|
| 12 |
+
|
| 13 |
+
Vous avez écrit un script qui post-corrige du texte OCR avec une
|
| 14 |
+
heuristique métier (par exemple : règles de normalisation propres
|
| 15 |
+
à un fonds d'archives donné). Vous voulez le brancher dans
|
| 16 |
+
Picarones pour mesurer son apport vs un baseline.
|
| 17 |
+
|
| 18 |
+
C'est exactement le cas que cible l'axe B (banc d'essai de
|
| 19 |
+
pipelines composées).
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## Module minimal
|
| 24 |
+
|
| 25 |
+
Un module Picarones est une **classe Python** qui hérite de
|
| 26 |
+
`BaseModule` et implémente `run(...)`.
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
# my_corrector.py
|
| 30 |
+
from picarones.domain.module_protocol import BaseModule
|
| 31 |
+
from picarones.domain.artifacts import ArtifactType, Artifact
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class MyCorrector(BaseModule):
|
| 35 |
+
"""Post-corrige le texte OCR avec une règle métier."""
|
| 36 |
+
|
| 37 |
+
input_types = (ArtifactType.TEXT,)
|
| 38 |
+
output_types = (ArtifactType.TEXT,)
|
| 39 |
+
|
| 40 |
+
def run(self, artifact: Artifact) -> Artifact:
|
| 41 |
+
text = artifact.payload
|
| 42 |
+
# Votre logique métier ici.
|
| 43 |
+
corrected = text.replace(" l'", " l'").replace(" ", " ")
|
| 44 |
+
return Artifact(
|
| 45 |
+
type=ArtifactType.TEXT,
|
| 46 |
+
payload=corrected,
|
| 47 |
+
)
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Quatre points à retenir :
|
| 51 |
+
|
| 52 |
+
1. `input_types` et `output_types` doivent être déclarés au niveau
|
| 53 |
+
classe (le planner les lit avant exécution).
|
| 54 |
+
2. `run` prend un `Artifact` et en retourne un. Pas d'effet de
|
| 55 |
+
bord, pas de mutation.
|
| 56 |
+
3. Le type de sortie peut différer du type d'entrée (par exemple
|
| 57 |
+
`IMAGE → TEXT` pour un OCR).
|
| 58 |
+
4. La classe ne doit rien savoir de Picarones au-delà de
|
| 59 |
+
`BaseModule` — c'est du Python ordinaire.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Manifeste
|
| 64 |
+
|
| 65 |
+
Pour être chargé, le module doit déclarer un manifeste avec
|
| 66 |
+
**5 champs obligatoires** :
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
from picarones.domain.module_protocol import ModuleManifest
|
| 70 |
+
|
| 71 |
+
MANIFEST = ModuleManifest(
|
| 72 |
+
name="my-corrector",
|
| 73 |
+
version="0.1.0",
|
| 74 |
+
author="Vous <vous@institution.fr>",
|
| 75 |
+
license="MIT",
|
| 76 |
+
description="Post-correction par règles métier.",
|
| 77 |
+
)
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Le manifeste sert à tracer **qui** est responsable du module dans
|
| 81 |
+
le rapport et à versionner les comparaisons longitudinales.
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## Audit
|
| 86 |
+
|
| 87 |
+
Avant exécution, le module passe un audit statique :
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
from picarones.evaluation.metrics.module_policy import audit_module
|
| 91 |
+
|
| 92 |
+
issues = audit_module(MyCorrector, MANIFEST)
|
| 93 |
+
assert not issues, f"Module non conforme : {issues}"
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Si l'audit échoue, le module n'est **pas chargé** dans la pipeline
|
| 97 |
+
— pas d'exception silencieuse en production. Les règles d'audit
|
| 98 |
+
sont énumérées dans
|
| 99 |
+
[`developer/module-policy.md`](../developer/module-policy.md).
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Brancher dans une pipeline
|
| 104 |
+
|
| 105 |
+
Une pipeline est décrite par un `PipelineSpec`. Le module est
|
| 106 |
+
référencé par son chemin Python :
|
| 107 |
+
|
| 108 |
+
```python
|
| 109 |
+
from picarones.domain.pipeline_spec import PipelineSpec, PipelineStep
|
| 110 |
+
|
| 111 |
+
spec = PipelineSpec(
|
| 112 |
+
name="ocr-puis-correction",
|
| 113 |
+
steps=[
|
| 114 |
+
PipelineStep(
|
| 115 |
+
name="ocr",
|
| 116 |
+
module="picarones.adapters.ocr.tesseract:TesseractAdapter",
|
| 117 |
+
),
|
| 118 |
+
PipelineStep(
|
| 119 |
+
name="post-correction",
|
| 120 |
+
module="my_corrector:MyCorrector",
|
| 121 |
+
),
|
| 122 |
+
],
|
| 123 |
+
)
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
Lancez le benchmark avec ce pipeline :
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
picarones run \
|
| 130 |
+
--corpus mon_corpus/ \
|
| 131 |
+
--pipeline ocr-puis-correction.yaml \
|
| 132 |
+
--output rapport.html
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
Le rapport présente alors **la pipeline complète** comme un
|
| 136 |
+
« moteur » à part entière, comparable aux autres dans le tableau
|
| 137 |
+
récapitulatif et le diagramme CD.
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## Étapes suivantes
|
| 142 |
+
|
| 143 |
+
- Politique normative et règles d'audit :
|
| 144 |
+
[`developer/module-policy.md`](../developer/module-policy.md)
|
| 145 |
+
- Étendre le moteur narratif pour commenter votre module :
|
| 146 |
+
[`developer/extending-i18n.md`](../developer/extending-i18n.md)
|
| 147 |
+
- Reproductibilité de la comparaison :
|
| 148 |
+
[`reference/reproducibility-snapshots.md`](../reference/reproducibility-snapshots.md)
|
| 149 |
+
- Architecture en cercles (où se branche un module) :
|
| 150 |
+
[`explanation/architecture.md`](../explanation/architecture.md)
|
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Garde-fou : tout lien interne dans ``docs/index.md`` doit pointer
|
| 2 |
+
vers un fichier réel.
|
| 3 |
+
|
| 4 |
+
Pourquoi ce test existe
|
| 5 |
+
-----------------------
|
| 6 |
+
|
| 7 |
+
``docs/index.md`` est l'**index canonique** de la documentation : il
|
| 8 |
+
est référencé depuis le README, depuis mkdocs.yml, et c'est la
|
| 9 |
+
première porte d'entrée pour un nouveau contributeur.
|
| 10 |
+
|
| 11 |
+
Avant Phase 1, ce fichier contenait 4 liens cassés (``first-benchmark``,
|
| 12 |
+
``writing-a-pipeline-module``, ``developer/narrative-engine``,
|
| 13 |
+
``user/...``) qui ont survécu pendant le rewrite parce qu'aucun test
|
| 14 |
+
ne validait ses propres liens. Ce garde-fou élimine la classe
|
| 15 |
+
d'erreur : si l'index ment, la CI échoue.
|
| 16 |
+
|
| 17 |
+
Périmètre
|
| 18 |
+
---------
|
| 19 |
+
|
| 20 |
+
On parse les liens markdown ``[texte](cible)`` et on vérifie que la
|
| 21 |
+
``cible`` :
|
| 22 |
+
|
| 23 |
+
- soit pointe vers un fichier existant (résolution relative à
|
| 24 |
+
``docs/`` ou à la racine pour les ``../X``) ;
|
| 25 |
+
- soit est une URL externe (``http://...``, ``mailto:...``) — non
|
| 26 |
+
vérifiée ici, c'est le rôle de tests externes ;
|
| 27 |
+
- soit est une ancre intra-document (``#section``) — non vérifiée.
|
| 28 |
+
|
| 29 |
+
Les liens vers des dossiers (``case-studies/``, ``audits/``) sont
|
| 30 |
+
vérifiés comme l'existence du dossier.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import re
|
| 36 |
+
from pathlib import Path
|
| 37 |
+
|
| 38 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 39 |
+
INDEX = REPO_ROOT / "docs" / "index.md"
|
| 40 |
+
|
| 41 |
+
#: Pattern markdown standard : ``[texte](cible)``. On capture la
|
| 42 |
+
#: cible (groupe 2) qu'on évaluera comme chemin.
|
| 43 |
+
_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _resolve_link(target: str) -> Path | None:
|
| 47 |
+
"""Résout une cible de lien relativement à ``docs/index.md``.
|
| 48 |
+
|
| 49 |
+
Retourne ``None`` si :
|
| 50 |
+
- URL externe (``http``, ``mailto``, ``#``) ;
|
| 51 |
+
- cible vide ;
|
| 52 |
+
- chemin qui ne se résout pas.
|
| 53 |
+
"""
|
| 54 |
+
target = target.strip()
|
| 55 |
+
|
| 56 |
+
# URL externe — pas notre problème ici.
|
| 57 |
+
if target.startswith(("http://", "https://", "mailto:", "#")):
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
# Retirer l'ancre éventuelle (``foo.md#section``)
|
| 61 |
+
target = target.split("#", 1)[0]
|
| 62 |
+
if not target:
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
# Les liens dans index.md sont relatifs à ``docs/``.
|
| 66 |
+
# Les liens vers la racine (``../GOVERNANCE.md``) doivent
|
| 67 |
+
# remonter au repo root.
|
| 68 |
+
base = INDEX.parent
|
| 69 |
+
resolved = (base / target).resolve()
|
| 70 |
+
return resolved
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_index_md_exists() -> None:
|
| 74 |
+
assert INDEX.exists(), (
|
| 75 |
+
f"{INDEX} absent — c'est l'index canonique de la doc, il "
|
| 76 |
+
"ne peut pas manquer."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_all_internal_links_in_index_resolve() -> None:
|
| 81 |
+
"""Tout lien interne dans ``docs/index.md`` doit pointer vers
|
| 82 |
+
un fichier ou dossier existant."""
|
| 83 |
+
text = INDEX.read_text(encoding="utf-8")
|
| 84 |
+
offenders: list[str] = []
|
| 85 |
+
for match in _LINK_RE.finditer(text):
|
| 86 |
+
target = match.group(2)
|
| 87 |
+
resolved = _resolve_link(target)
|
| 88 |
+
if resolved is None:
|
| 89 |
+
continue # URL externe / ancre — pas notre périmètre
|
| 90 |
+
if not resolved.exists():
|
| 91 |
+
offenders.append(
|
| 92 |
+
f" « {match.group(1)} » → {target!r} "
|
| 93 |
+
f"(résolu vers {resolved.relative_to(REPO_ROOT) if resolved.is_relative_to(REPO_ROOT) else resolved})"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
assert not offenders, (
|
| 97 |
+
f"{len(offenders)} lien(s) cassé(s) dans docs/index.md :\n"
|
| 98 |
+
+ "\n".join(offenders)
|
| 99 |
+
+ "\n\n→ Soit créer le fichier cible, soit corriger le lien."
|
| 100 |
+
)
|