File size: 8,214 Bytes
71f166b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9011070
71f166b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9011070
71f166b
 
 
 
 
 
 
 
9011070
71f166b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# Tolérances de régression — legacy ↔ rewrite

> **Audience** : développeur qui migre une fonctionnalité legacy
> vers le rewrite, reviewer qui relit la PR.
>
> **Référence** : [`legacy-retirement-plan.md`](legacy-retirement-plan.md).
>
> **Contrat** : le harness `tests/regression/legacy_vs_rewrite/`
> exécute legacy + rewrite sur les mêmes corpus de référence et
> compare leurs sorties.  Toute divergence au-delà de la tolérance
> ε définie ici est une **régression à corriger avant merge**.
>
> Une régression peut être :
>
> - **Intentionnelle** : la phase de migration corrige un bug
>   historique → la tolérance est temporairement relâchée AVEC
>   commentaire pointant vers l'issue.
> - **Inattendue** : c'est ce que ce document est censé empêcher.

## Principe général

Pour une fonctionnalité donnée, la sortie du rewrite **doit être
égale** à celle du legacy à la tolérance ε près.  L'égalité est :

- **Bit-for-bit** quand l'output est déterministe (texte, hash, JSON).
- **Sémantique** quand l'output structurel a des libertés (ordre des
  éléments d'un set, indentation HTML, ordre des facts narratifs
  équivalents).

## Table des tolérances par type d'output

### Métriques numériques

| Métrique | ε | Justification |
|----------|---|---------------|
| `cer_raw`, `cer_nfc`, `cer_caseless`, `cer_diplomatic` | **0** (bit-for-bit) | jiwer est déterministe ; toute différence = changement de pré/post-processing |
| `wer`, `mer`, `wil` | **0** | idem |
| `bleu`, `chrf` | **1e-9** | flottants — réordonnancements internes acceptables |
| `precision`, `recall`, `f1` (NER) | **1e-9** | flottants |
| `mufi_coverage`, `abbreviation_expansion_score` | **0** | comptage entier sur ensembles fermés |
| `roman_numerals_accuracy` | **0** | parsing déterministe |
| `unicode_blocks_accuracy` | **0** | tables Unicode déterministes |
| `reading_order_f1` (ICDAR 2015) | **1e-9** | algorithme déterministe, flottants |
| `layout_f1` | **1e-9** | flottants |
| `confusion_matrix.entries` | **0** | comptage entier |
| `taxonomy.error_class_*` | **0** | classification déterministe sur règles |

### Tests statistiques

| Test | ε | Justification |
|------|---|---------------|
| Wilcoxon `p_value` | **1e-9** | scipy `wilcoxon` est déterministe à entrée constante |
| Friedman `chi2`, `p_value` | **1e-9** | idem |
| Nemenyi (matrice p-values) | **1e-9** | dérivé de Friedman |
| Bootstrap CI 95 % | **1e-3** | random seed FIXÉ explicitement (cf. `bootstrap.py` du legacy : `seed=42`) ; la tolérance laisse une marge minuscule pour les ré-implémentations qui itéreraient dans un ordre différent à seed identique |
| Pareto front (set d'engines dominants) | **0** (bit-for-bit en tant qu'ensemble) | dominance Pareto stable sur entrées identiques |
| CDD (Critical Difference Diagram) coordonnées SVG | **1e-3** sur les positions (px) | rendu Matplotlib peut varier sur des sub-pixels selon backend |
| Clustering (labels) | **0** sur l'**ensemble** des classes (l'étiquetage interne 0/1/2 peut différer mais la partition doit être identique) | un test custom compare les partitions, pas les labels |
| Corrélation Spearman / Pearson | **1e-9** | flottants |

### Calibration

| Output | ε | Justification |
|--------|---|---------------|
| ECE, MCE | **1e-9** | flottants, pas d'aléatoire |
| Reliability diagram (bins, freq, conf) | **0** sur les bins, **1e-9** sur les valeurs | binning déterministe |

### Confidences sidecar (S50 sur Tesseract)

| Output | ε |
|--------|---|
| `tokens[].text` | **0** (string identique) |
| `tokens[].confidence` | **0** | Tesseract retourne un entier 0-100 ; division exacte par 100 → flottant binairement identique en IEEE-754 |
| `extractor`, `model_version` | **0** |

### HTML (rapport `reports/html/render.py`)

Le diff HTML est **structurel**, pas lexical :

- Mêmes éléments DOM avec mêmes attributs sémantiques (`data-*`,
  `aria-*`, `id`, `class`).
- Mêmes valeurs textuelles dans les nœuds de texte.
- L'**ordre** des sections doit être identique.
- L'indentation et le whitespace inter-éléments sont **ignorés**.
- Le contenu d'un `<script>` est comparé après normalisation
  d'espace blanc.

Implémenté via une fonction `assert_html_semantically_equal(a, b)`
qui parse les deux HTML avec `lxml` (ou `html.parser` fallback) et
compare l'arbre.

### CSV (`reports/csv/render.py`)

| Output | ε |
|--------|---|
| Header row | **0** (identique exact) |
| Data rows (set non ordonné) | **0** sur l'ensemble |
| Ordre des lignes | autorisé à différer | les renderers triaient parfois différemment ; seule l'égalité ensembliste est exigée |
| Format des nombres | **0** (le rewrite formate à 6 décimales `f"{v:.6f}"`) | déterministe |

### JSON (`reports/json/render.py`)

| Output | ε |
|--------|---|
| Bit-for-bit identique | **0** | le rewrite utilise `model_dump(mode="json")` Pydantic + `json.dumps(sort_keys=True, indent=2, ensure_ascii=False)` ; le legacy doit être amené au même contrat dans la phase concernée |

### Narrative facts (Phase 3)

| Aspect | ε |
|--------|---|
| Ensemble des `Fact` produits (par `FactType`) | **0** sur l'ensemble | l'arbitre peut réordonner mais pas inventer ni rater un fact |
| Payload de chaque fact (les valeurs numériques citées) | **0** (bit-for-bit) | garde-fou anti-hallucination |
| Templates rendus FR + EN | **0** sur le texte | déterministe par `str.format_map` |
| Ordre final des facts dans la synthèse | **autorisé à différer** | l'arbitre du rewrite peut choisir un ordre différent si la priorité est respectée — un test custom valide « les facts HIGH apparaissent avant les MEDIUM » plutôt que l'ordre exact |

### Rapport HTML — sections legacy spécifiques (Phase 5)

Pour chaque renderer migré (calibration, NER, Pareto, narrative,
philological, etc.), un cas-test de régression dédié vit dans
`tests/regression/legacy_vs_rewrite/test_phase5_<renderer>.py`.
Le snapshot legacy est figé en début de phase.

## Aléatoire — politique

Tout module qui utilise `random` doit :

1. Accepter un argument `seed: int` ou utiliser une seed fixée
   explicitement.
2. Documenter la seed dans son docstring.
3. Le harness de régression utilise toujours **seed=42**.

Modules concernés au legacy :

- `measurements/statistics/bootstrap.py` (seed=42)
- `measurements/runner/workers.py` (pas d'aléatoire — confirmé)
- `core/results.py` (pas d'aléatoire — confirmé)

## Adaptateurs cloud (Mistral, OpenAI, Anthropic, Google, Azure)

Les appels réseau ne sont **pas** rejoués pendant la régression —
le test serait non-déterministe et coûteux.  Stratégie :

1. Le harness utilise des **fixtures de réponses figées** (JSON
   capturé en local lors de la création du corpus de référence).
2. Le legacy et le rewrite reçoivent **la même fixture** ; le test
   vérifie que tous deux produisent le même output structurel.
3. Si une dépendance SDK change la sérialisation (rare), le test
   pète bruyamment et la PR doit re-frigorifier la fixture.

Aucune tolérance non triviale n'est nécessaire — l'égalité
bit-for-bit est tenable parce que l'aléatoire vient du cloud, pas
du parser.

## Procédure d'exception (régression intentionnelle)

Quand une migration corrige un bug historique légitime :

1. Ouvrir une issue GitHub avec le label `regression-intentional`.
2. Référencer le numéro d'issue dans le commit qui modifie la
   tolérance.
3. Ajouter une entrée dans la section *« Régressions intentionnelles
   acceptées »* ci-dessous, **avant** le merge.
4. La tolérance peut être relâchée temporairement ; au merge, soit
   le snapshot legacy est mis à jour pour refléter le nouveau
   comportement (correct), soit la tolérance reste serrée pour les
   prochaines migrations.

## Régressions intentionnelles acceptées

| Date | Issue | Phase | Module | Description |
|------|-------|-------|--------|-------------|
| (aucune à ce jour) |  |  |  |  |

## Révisions

| Version | Date | Changements |
|---------|------|-------------|
| 1.0 | 2026-05 | Création initiale (Phase 0 du plan de retrait legacy) |