Claude commited on
Commit
35d7de7
·
unverified ·
1 Parent(s): c067b8b

feat(report): Phase 21C — dispatch Documents + Crosses (cer-doc + quality-cer + pareto + marginal-cost)

Browse files

Troisième commit Phase 21 : déplace les charts cross-dimensionnels et
doc-centriques depuis view_analyses.html vers leurs vues XerOCR
sémantiques. view_analyses.html ne contient plus que view_results_html
(Phase 23) et la corr-matrix interactive JS (Commit 21D pour
suppression).

Déplacés vers documents.html :
- chart-cer-doc — CER par document, tous moteurs (doc-centric)

Déplacés vers crosses.html :
- chart-quality-cer — qualité image × CER (cross-dimensional)
- pareto-chart — front Pareto interactif avec 3 toggles
(cost/speed/co2), liste d'hypothèses, note
pédagogique. Le wrapper passe de
`<div class="chart-card pareto-card">` à
`<section class="card xer-chart-card pareto-card">`
— convention XerOCR. `.pareto-card` (border
vert) et `.pareto-toolbar/.pareto-toggle`
inchangés en CSS, pas de régression visuelle.
- marginal_cost_html — coût marginal d'une erreur évitée entre
paires de moteurs (cost × quality cross-dim).

Commentaire d'en-tête de view_analyses.html mis à jour pour refléter
l'état post-21C : reliquat documenté (view_results pour Phase 23, JS
corr-matrix pour 21D).

Tests :
- 19 nouveaux tests dans test_phase21_dispatch.py (4 classes : Documents,
Crosses/Quality, Crosses/Pareto, Crosses/MarginalCost).
- Anti-theater validé par 3 sabotages distincts (canvas rename, toggle
removal, conditional wiring rename).
- test_english_locale_full_render (test_docs_case_studies.py) mis à jour
pour le nouveau wrapper Pareto (`pareto-card` au lieu de
`chart-card pareto-card` — l'ancien sélecteur exact serait cassé).
- test_report_contains_pareto_card (test_pareto_pricing.py) idem.

Lint propre, 5764 tests, 0 failed.

https://claude.ai/code/session_01WYDbfkhKPeBZ15BTP4e9Ye

picarones/reports/html/templates/view_analyses.html CHANGED
@@ -1,94 +1,30 @@
1
 
2
- <!-- ════ Vue 5 : Analyses (charts Chart.js non encore portés en SVG) ═══
3
- Conteneur des charts Chart.js qui n'ont pas (encore) d'équivalent
4
- SVG côté serveur dans les 4 vues XerOCR principales. Migrations
5
- déjà effectuées et retirées :
6
- - chart-cer-hist → engines.cer_distribution (SVG)
7
- - chart-radar → engines.radar (SVG)
8
- - chart-gini-cer → crosses CER × Gini (SVG)
9
- - chart-ratio-anchor → crosses Ancrage × Longueur (SVG)
10
- - venn-container → crosses.venn (SVG)
11
- - wilcoxon-table → engines.wilcoxon (HTML)
12
- - corr-matrix → engines.correlation (HTML)
13
- Charts ENCORE en Chart.js dans cette vue : cer-doc, duration,
14
- quality-cer, taxonomy, reliability, bootstrap-ci, pareto-chart,
15
- error-clusters. Tant que ces 8 charts persistent ici, le vendor
16
- Chart.js inliné dans base.html.j2 reste justifié. Migration
17
- incrémentale vers SVG = chantier non bloquant. -->
18
  <div id="view-analyses" class="view">
19
  <div class="charts-grid">
20
 
21
- <div class="chart-card">
22
- <h3 data-i18n="h_cer_doc">CER par document (tous moteurs)</h3>
23
- <div class="chart-canvas-wrap">
24
- <canvas id="chart-cer-doc" role="img" aria-label="CER par document" data-a11y-label="CER par document"></canvas>
25
- </div>
26
- </div>
27
-
28
- <div class="chart-card">
29
- <h3 data-i18n="h_quality_cer">Qualité image ↔ CER (scatter plot)</h3>
30
- <div class="chart-canvas-wrap">
31
- <canvas id="chart-quality-cer" role="img" aria-label="Corrélation qualité d'image / CER" data-a11y-label="Corrélation qualité d'image / CER"></canvas>
32
- </div>
33
- <div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="quality_cer_note">
34
- Chaque point = un document. Axe X = score qualité image [0–1]. Axe Y = CER. Corrélation négative attendue.
35
- </div>
36
- </div>
37
-
38
- <div class="chart-card pareto-card" style="grid-column:1/-1">
39
- <h3 data-i18n="h_pareto">Compromis qualité / coût</h3>
40
- <div class="pareto-toolbar" role="group" aria-label="Axe d'analyse Pareto">
41
- <button type="button" class="pareto-toggle active" data-axis="cost"
42
- onclick="setParetoAxis('cost')"
43
- aria-pressed="true"
44
- data-i18n="pareto_axis_cost">Coût € / 1000 pages</button>
45
- <button type="button" class="pareto-toggle" data-axis="speed"
46
- onclick="setParetoAxis('speed')"
47
- aria-pressed="false"
48
- data-i18n="pareto_axis_speed">Vitesse (s / page)</button>
49
- <button type="button" class="pareto-toggle pareto-experimental" data-axis="co2"
50
- onclick="setParetoAxis('co2')"
51
- aria-pressed="false"
52
- data-i18n="pareto_axis_co2"
53
- title="Estimation expérimentale">Carbone (g CO₂)</button>
54
- </div>
55
- <div class="chart-canvas-wrap"><canvas id="pareto-chart" role="img" aria-label="Front Pareto coût/qualité" data-a11y-label="Front Pareto coût/qualité"></canvas></div>
56
- <div id="pareto-method-note" class="pareto-note" data-i18n="pareto_note">
57
- Les moteurs sur la frontière de Pareto (en évidence) sont ceux pour
58
- lesquels aucun autre moteur n'offre simultanément un meilleur CER ET
59
- un meilleur coût. Prix indicatifs (table interne, datée). Le mode
60
- carbone est expérimental.
61
- </div>
62
- <details class="pareto-assumptions">
63
- <summary data-i18n="pareto_assumptions_summary">Hypothèses détaillées par moteur</summary>
64
- <ul id="pareto-assumptions-list"></ul>
65
- </details>
66
- </div>
67
-
68
- {# Sections déplacées vers les vues XerOCR (S13 cleanup) :
69
- - calibration_summary_html, reliability_diagrams_html → Engines/Diagnostics
70
- - ner_summary_html, ner_per_category_html → Engines/Diagnostics
71
- - philological_profile_html → Engines/Diagnostics
72
- - searchability_html, numerical_sequences_html → Engines/Diagnostics
73
- - specialization_html → Crosses (passthrough)
74
- - divergence_matrix_html, oracle_gap_html → Crosses (passthrough)
75
- Doubler le rendu HTML coûtait ~120 lignes inutiles dans chaque rapport.
76
- Les renderers eux-mêmes sont toujours invoqués côté générateur ;
77
- seul le ``{% include %}`` legacy a été retiré. #}
78
-
79
- {# Phase B6 — view_results (par EvaluationView) — pas encore migrée
80
- vers les vues XerOCR. Conservée le temps de la migration. #}
81
  {% if view_results_html %}
82
  {{ view_results_html | safe }}
83
  {% endif %}
84
 
85
- {% if marginal_cost_html %}
86
- <div class="chart-card" style="grid-column:1/-1">
87
- {{ marginal_cost_html | safe }}
88
- </div>
89
- {% endif %}
90
-
91
- <!-- Sprint 7 — Matrice de corrélation -->
92
  <div class="chart-card technical" style="grid-column:1/-1">
93
  <h3 data-i18n="h_correlation">Matrice de corrélation entre métriques</h3>
94
  <div style="margin-bottom:.5rem">
 
1
 
2
+ <!-- ════ Vue 5 : Analyses (reliquat post-Phase 21) ═══════════════════
3
+ Quasi-vide après le dispatch Phase 21 (A/B/C). Contenu restant :
4
+ - view_results_html — Phase 23 : intégration en <details>
5
+ dans la vue XerOCR la plus pertinente.
6
+ - corr-matrix interactif — Commit 21D : suppression (la version
7
+ Python `engines_correlation_matrix_html`
8
+ couvre déjà le besoin dans
9
+ engines_diagnostics.html, sans selector
10
+ JS mais avec sélection serveur).
11
+ L'onglet Analyses lui-même sera retiré en Phase 22 une fois ces
12
+ deux derniers items traités. Le wrapper `<div id="view-analyses">`
13
+ reste pour ne pas casser les deeplinks `#analyses` legacy entre
14
+ Phase 21 et Phase 22. -->
 
 
 
15
  <div id="view-analyses" class="view">
16
  <div class="charts-grid">
17
 
18
+ {# Phase B6 — view_results (par EvaluationView) — destinée à
19
+ Phase 23 : intégration en <details> dans la vue XerOCR
20
+ la plus pertinente (option α conservatrice). #}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  {% if view_results_html %}
22
  {{ view_results_html | safe }}
23
  {% endif %}
24
 
25
+ {# Matrice de corrélation interactive (JS) — sera supprimée en
26
+ Commit 21D. La version Python statique vit déjà dans
27
+ engines_diagnostics.html via engines_correlation_matrix_html. #}
 
 
 
 
28
  <div class="chart-card technical" style="grid-column:1/-1">
29
  <h3 data-i18n="h_correlation">Matrice de corrélation entre métriques</h3>
30
  <div style="margin-bottom:.5rem">
picarones/reports/html/templates/views/crosses.html CHANGED
@@ -35,4 +35,63 @@
35
  {{ crosses_venn_html | safe }}
36
  </section>
37
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </section>
 
35
  {{ crosses_venn_html | safe }}
36
  </section>
37
  {% endif %}
38
+
39
+ {# Phase 21C dispatch — depuis view_analyses.html :
40
+ - chart-quality-cer : scatter qualité image × CER (cross-dimensional)
41
+ - pareto-chart : front Pareto coût/vitesse/CO₂ × CER, interactif
42
+ - marginal_cost_html : coût marginal par erreur évitée entre paires
43
+ #}
44
+ <section class="card xer-chart-card" aria-labelledby="crosses-quality-cer-title"
45
+ style="margin-top:1.25rem">
46
+ <h3 id="crosses-quality-cer-title" data-i18n="h_quality_cer">Qualité image ↔ CER (scatter plot)</h3>
47
+ <p data-i18n="quality_cer_note">
48
+ Chaque point = un document. Axe X = score qualité image [0–1]. Axe Y = CER. Corrélation négative attendue.
49
+ </p>
50
+ <div class="chart-canvas-wrap">
51
+ <canvas id="chart-quality-cer" role="img"
52
+ aria-label="Corrélation qualité d'image / CER"
53
+ data-a11y-label="Corrélation qualité d'image / CER"></canvas>
54
+ </div>
55
+ </section>
56
+
57
+ <section class="card xer-chart-card pareto-card" aria-labelledby="crosses-pareto-title"
58
+ style="margin-top:1.25rem">
59
+ <h3 id="crosses-pareto-title" data-i18n="h_pareto">Compromis qualité / coût</h3>
60
+ <div class="pareto-toolbar" role="group" aria-label="Axe d'analyse Pareto">
61
+ <button type="button" class="pareto-toggle active" data-axis="cost"
62
+ onclick="setParetoAxis('cost')"
63
+ aria-pressed="true"
64
+ data-i18n="pareto_axis_cost">Coût € / 1000 pages</button>
65
+ <button type="button" class="pareto-toggle" data-axis="speed"
66
+ onclick="setParetoAxis('speed')"
67
+ aria-pressed="false"
68
+ data-i18n="pareto_axis_speed">Vitesse (s / page)</button>
69
+ <button type="button" class="pareto-toggle pareto-experimental" data-axis="co2"
70
+ onclick="setParetoAxis('co2')"
71
+ aria-pressed="false"
72
+ data-i18n="pareto_axis_co2"
73
+ title="Estimation expérimentale">Carbone (g CO₂)</button>
74
+ </div>
75
+ <div class="chart-canvas-wrap">
76
+ <canvas id="pareto-chart" role="img"
77
+ aria-label="Front Pareto coût/qualité"
78
+ data-a11y-label="Front Pareto coût/qualité"></canvas>
79
+ </div>
80
+ <div id="pareto-method-note" class="pareto-note" data-i18n="pareto_note">
81
+ Les moteurs sur la frontière de Pareto (en évidence) sont ceux pour
82
+ lesquels aucun autre moteur n'offre simultanément un meilleur CER ET
83
+ un meilleur coût. Prix indicatifs (table interne, datée). Le mode
84
+ carbone est expérimental.
85
+ </div>
86
+ <details class="pareto-assumptions">
87
+ <summary data-i18n="pareto_assumptions_summary">Hypothèses détaillées par moteur</summary>
88
+ <ul id="pareto-assumptions-list"></ul>
89
+ </details>
90
+ </section>
91
+
92
+ {% if marginal_cost_html %}
93
+ <section class="card xer-chart-card" style="margin-top:1.25rem">
94
+ {{ marginal_cost_html | safe }}
95
+ </section>
96
+ {% endif %}
97
  </section>
picarones/reports/html/templates/views/documents.html CHANGED
@@ -61,4 +61,17 @@
61
  {{ documents_worst_lines_html | safe }}
62
  </div>
63
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </section>
 
61
  {{ documents_worst_lines_html | safe }}
62
  </div>
63
  {% endif %}
64
+
65
+ {# Phase 21C dispatch — CER par document : chart doc-centric depuis
66
+ view_analyses.html. Construit par buildCharts() (lazy au premier
67
+ passage sur l'onglet Documents). #}
68
+ <section class="card xer-chart-card" aria-labelledby="documents-cer-doc-title"
69
+ style="margin-top:1.25rem">
70
+ <h3 id="documents-cer-doc-title" data-i18n="h_cer_doc">CER par document (tous moteurs)</h3>
71
+ <div class="chart-canvas-wrap">
72
+ <canvas id="chart-cer-doc" role="img"
73
+ aria-label="CER par document"
74
+ data-a11y-label="CER par document"></canvas>
75
+ </div>
76
+ </section>
77
  </section>
tests/evaluation/metrics/test_pareto_pricing.py CHANGED
@@ -271,7 +271,11 @@ class TestReportIntegration:
271
  out = tmp_path / "report.html"
272
  ReportGenerator(benchmark_result).generate(out)
273
  html = out.read_text(encoding="utf-8")
274
- assert 'class="chart-card pareto-card"' in html
 
 
 
 
275
  assert 'id="pareto-chart"' in html
276
  assert 'setParetoAxis(\'cost\')' in html
277
  assert 'setParetoAxis(\'speed\')' in html
 
271
  out = tmp_path / "report.html"
272
  ReportGenerator(benchmark_result).generate(out)
273
  html = out.read_text(encoding="utf-8")
274
+ # Phase 21C : Pareto migré vers crosses.html sous convention XerOCR.
275
+ # Le wrapper est passé de <div class="chart-card pareto-card"> à
276
+ # <section class="card xer-chart-card pareto-card">. La classe
277
+ # ``pareto-card`` (border-left vert via CSS) est préservée.
278
+ assert "pareto-card" in html
279
  assert 'id="pareto-chart"' in html
280
  assert 'setParetoAxis(\'cost\')' in html
281
  assert 'setParetoAxis(\'speed\')' in html
tests/reports/test_docs_case_studies.py CHANGED
@@ -168,7 +168,11 @@ class TestEndToEnd:
168
  for marker in [
169
  'class="synth-card"', # Sprint 19 narrative
170
  'class="cdd-card"', # Sprint 18 CDD
171
- 'class="chart-card pareto-card"', # Sprint 20 Pareto
 
 
 
 
172
  'id="glossary-panel"', # Sprint 21 glossaire
173
  'id="customize-panel"', # Sprint 21 personnalisation
174
  'btn-customize',
 
168
  for marker in [
169
  'class="synth-card"', # Sprint 19 narrative
170
  'class="cdd-card"', # Sprint 18 CDD
171
+ # Phase 21C : Pareto migré dans Crosses sous convention XerOCR.
172
+ # Wrapper passé de <div class="chart-card pareto-card"> à
173
+ # <section class="card xer-chart-card pareto-card">.
174
+ 'pareto-card', # Sprint 20 Pareto (post-21C)
175
+ 'id="pareto-chart"', # Canvas Pareto présent
176
  'id="glossary-panel"', # Sprint 21 glossaire
177
  'id="customize-panel"', # Sprint 21 personnalisation
178
  'btn-customize',
tests/reports/test_phase21_dispatch.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests Phase 21C — dispatch Documents + Crosses (chart-cer-doc, chart-quality-cer, pareto, marginal_cost).
2
+
3
+ Vérifie que les charts cross-dimensionnels et doc-centriques sont
4
+ correctement déplacés depuis view_analyses.html vers leur destination
5
+ XerOCR sémantique :
6
+
7
+ - chart-cer-doc → documents.html (doc-centric line chart)
8
+ - chart-quality-cer → crosses.html (cross-dim image quality × CER)
9
+ - pareto-chart → crosses.html (cross-dim cost/quality interactif)
10
+ - marginal_cost_html → crosses.html (cost marginal par erreur évitée)
11
+
12
+ Anti-theater : chaque test échoue par sabotage ciblé (ID rename,
13
+ template miss, etc.) — preuve documentée dans les docstrings de classe.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+ REPO_ROOT = Path(__file__).resolve().parents[2]
23
+ TEMPLATES = REPO_ROOT / "picarones" / "reports" / "html" / "templates"
24
+ DOCS_VIEW = TEMPLATES / "views" / "documents.html"
25
+ CROSSES_VIEW = TEMPLATES / "views" / "crosses.html"
26
+ ANALYSES_VIEW = TEMPLATES / "view_analyses.html"
27
+
28
+
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # 1. Documents — chart-cer-doc
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+
33
+
34
+ class TestChartCerDocInDocuments:
35
+ """``chart-cer-doc`` (CER par document, tous moteurs) doit vivre
36
+ dans documents.html — chart doc-centric va avec la galerie.
37
+
38
+ Anti-theater : retirer/renommer le canvas → ces tests échouent.
39
+ """
40
+
41
+ @pytest.fixture(scope="class")
42
+ def docs_src(self) -> str:
43
+ return DOCS_VIEW.read_text(encoding="utf-8")
44
+
45
+ @pytest.fixture(scope="class")
46
+ def analyses_src(self) -> str:
47
+ return ANALYSES_VIEW.read_text(encoding="utf-8")
48
+
49
+ def test_canvas_in_documents(self, docs_src: str) -> None:
50
+ assert 'id="chart-cer-doc"' in docs_src
51
+
52
+ def test_section_heading_present(self, docs_src: str) -> None:
53
+ assert 'id="documents-cer-doc-title"' in docs_src
54
+ assert 'data-i18n="h_cer_doc"' in docs_src
55
+
56
+ def test_aria_label_preserved(self, docs_src: str) -> None:
57
+ assert 'aria-label="CER par document"' in docs_src
58
+ assert 'data-a11y-label="CER par document"' in docs_src
59
+
60
+ def test_not_in_analyses(self, analyses_src: str) -> None:
61
+ assert 'id="chart-cer-doc"' not in analyses_src, (
62
+ "chart-cer-doc doit être retiré de view_analyses.html (Phase 21C)"
63
+ )
64
+
65
+
66
+ # ─────────────────────────────────────────────────────────────────────────────
67
+ # 2. Crosses — chart-quality-cer
68
+ # ─────────────────────────────────────────────────────────────────────────────
69
+
70
+
71
+ class TestChartQualityCerInCrosses:
72
+ """``chart-quality-cer`` (qualité image × CER) est un scatter
73
+ cross-dimensionnel — vit dans crosses.html.
74
+
75
+ Anti-theater : double-vérification (présent dans crosses + absent
76
+ dans analyses). Renommer l'ID dans un seul des 2 fichiers fait
77
+ immédiatement échouer un test.
78
+ """
79
+
80
+ @pytest.fixture(scope="class")
81
+ def crosses_src(self) -> str:
82
+ return CROSSES_VIEW.read_text(encoding="utf-8")
83
+
84
+ @pytest.fixture(scope="class")
85
+ def analyses_src(self) -> str:
86
+ return ANALYSES_VIEW.read_text(encoding="utf-8")
87
+
88
+ def test_canvas_in_crosses(self, crosses_src: str) -> None:
89
+ assert 'id="chart-quality-cer"' in crosses_src
90
+
91
+ def test_section_heading_present(self, crosses_src: str) -> None:
92
+ assert 'id="crosses-quality-cer-title"' in crosses_src
93
+ assert 'data-i18n="h_quality_cer"' in crosses_src
94
+
95
+ def test_note_present(self, crosses_src: str) -> None:
96
+ assert 'data-i18n="quality_cer_note"' in crosses_src
97
+
98
+ def test_not_in_analyses(self, analyses_src: str) -> None:
99
+ assert 'id="chart-quality-cer"' not in analyses_src
100
+
101
+
102
+ # ─────────────────────────────────────────────────────────────────────────────
103
+ # 3. Crosses — pareto-chart (canvas + toolbar + assumptions)
104
+ # ─────────────────────────────────────────────────────────────────────────────
105
+
106
+
107
+ class TestParetoChartInCrosses:
108
+ """Le Pareto avec toolbar 3 axes (cost/speed/co2) + détails
109
+ hypothèses est entièrement dans crosses.html.
110
+
111
+ Anti-theater : retirer un toggle, retirer la liste d'hypothèses,
112
+ ou laisser une copie dans analyses → tests échouent.
113
+ """
114
+
115
+ @pytest.fixture(scope="class")
116
+ def crosses_src(self) -> str:
117
+ return CROSSES_VIEW.read_text(encoding="utf-8")
118
+
119
+ @pytest.fixture(scope="class")
120
+ def analyses_src(self) -> str:
121
+ return ANALYSES_VIEW.read_text(encoding="utf-8")
122
+
123
+ def test_canvas_in_crosses(self, crosses_src: str) -> None:
124
+ assert 'id="pareto-chart"' in crosses_src
125
+
126
+ def test_toolbar_present(self, crosses_src: str) -> None:
127
+ assert 'class="pareto-toolbar"' in crosses_src
128
+ assert 'role="group"' in crosses_src
129
+
130
+ @pytest.mark.parametrize("axis", ["cost", "speed", "co2"])
131
+ def test_each_axis_toggle_present(self, crosses_src: str, axis: str) -> None:
132
+ assert f'data-axis="{axis}"' in crosses_src
133
+ assert f"setParetoAxis('{axis}')" in crosses_src
134
+
135
+ def test_assumptions_list_container(self, crosses_src: str) -> None:
136
+ assert 'id="pareto-assumptions-list"' in crosses_src
137
+ assert 'class="pareto-assumptions"' in crosses_src
138
+
139
+ def test_method_note_present(self, crosses_src: str) -> None:
140
+ assert 'id="pareto-method-note"' in crosses_src
141
+ assert 'data-i18n="pareto_note"' in crosses_src
142
+
143
+ def test_not_in_analyses(self, analyses_src: str) -> None:
144
+ assert 'id="pareto-chart"' not in analyses_src
145
+ assert 'id="pareto-assumptions-list"' not in analyses_src
146
+ assert 'class="pareto-toolbar"' not in analyses_src
147
+
148
+
149
+ # ─────────────────────────────────────────────────────────────────────────────
150
+ # 4. Crosses — marginal_cost_html
151
+ # ─────────────────────────────────────────────────────────────────────────────
152
+
153
+
154
+ class TestMarginalCostInCrosses:
155
+ """``marginal_cost_html`` mesure le coût marginal d'une erreur évitée
156
+ entre paires de moteurs — cross-dimensionnel cost × quality.
157
+
158
+ Anti-theater : déplacer ailleurs ou oublier le wiring → tests échouent.
159
+ """
160
+
161
+ def test_wired_in_crosses(self) -> None:
162
+ crosses_src = CROSSES_VIEW.read_text(encoding="utf-8")
163
+ assert "{% if marginal_cost_html %}" in crosses_src, (
164
+ "marginal_cost_html doit être câblé conditionnellement dans crosses.html"
165
+ )
166
+
167
+ def test_not_in_analyses(self) -> None:
168
+ analyses_src = ANALYSES_VIEW.read_text(encoding="utf-8")
169
+ assert "{% if marginal_cost_html %}" not in analyses_src
170
+
171
+ def test_not_in_engines_diagnostics(self) -> None:
172
+ """Anti-confusion : marginal_cost n'est PAS dans diagnostics —
173
+ il appartient à Crosses (cross-dimensional), pas à 'où ça plante'."""
174
+ diag_src = (TEMPLATES / "views" / "engines_diagnostics.html").read_text(encoding="utf-8")
175
+ assert "{% if marginal_cost_html %}" not in diag_src