Claude commited on
Commit
32c3118
·
unverified ·
1 Parent(s): d349d11

docs(index): repair broken links + lock against drift

Browse files

docs/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 CHANGED
@@ -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](developer/narrative-engine.md) ([EN](developer/narrative-engine.en.md))
54
- 5. Écrire un module pour le banc d'essai : [`user/writing-a-pipeline-module.md`](user/writing-a-pipeline-module.md)
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 post-rewrite** :
150
- `domain → formats → evaluation → pipeline → adapters → app → reports_v2 → interfaces`.
151
- L'arbo legacy `picarones/{cli,web,engines,llm,pipelines,report}/`
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` (baseline 73,
155
- doit décroître).
156
- - **Les chiffres en prose qui dépendent de l'état du code** (compte
157
- de tests, nombre d'adapters) sont régénérés par
158
- `scripts/gen_readme_tables.py` modifier le code, pas la doc.
159
- - **Cohérence FR/EN** : un fichier `xxx.md` en FR + un fichier
160
- `xxx.en.md` en EN miroir. Pas de fragments mêlés.
 
 
 
 
 
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.
docs/tutorials/first-benchmark.md ADDED
@@ -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/)
docs/tutorials/writing-a-pipeline-module.md ADDED
@@ -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)
tests/docs/test_index_links_resolve.py ADDED
@@ -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
+ )