Claude commited on
Commit
7a072e2
·
unverified ·
1 Parent(s): 1acc64c

refactor(measurements): promouvoir modules philologiques/académiques/governance depuis extras/

Browse files

Les modules suivants vivaient dans ``picarones/extras/`` (cercle 3,
"plugins") alors qu'ils sont enregistrés dans le ``metric_registry``
et calculés automatiquement par le runner — ils sont en fait des
métriques officielles (cercle 2).

Modules philologiques (``extras/historical/`` → ``measurements/``) :
abbreviations, early_modern_typography, lexical_modernization,
modern_archives, mufi, philological_runner, roman_numerals,
unicode_blocks.

Modules académiques (``extras/academic/`` → ``measurements/``) :
image_predictive, taxonomy_cooccurrence, taxonomy_intra_doc.

Module governance (``extras/governance/`` → ``measurements/``) :
module_policy.

Les sous-packages ``extras/{historical,academic,governance}/`` sont
supprimés (vides après promotion). Tests de migration phaseA/B
(qui validaient ces locations transitoires) supprimés — leur rôle
est désormais documenté dans ``docs/architecture.md``.

https://claude.ai/code/session_01Hsd7kL8yeCbXn1mA7GQK9L

picarones/extras/academic/__init__.py DELETED
@@ -1,18 +0,0 @@
1
- """Modules techniques sans cas d'usage prod direct.
2
-
3
- Ces 3 modules calculent des distributions intéressantes pour la
4
- recherche académique mais ne participent pas à la décision
5
- *« peut-on déployer ce moteur en prod ? »*.
6
-
7
- Modules
8
- -------
9
- - :mod:`taxonomy_intra_doc` — heatmap classe×position intra-document.
10
- - :mod:`taxonomy_cooccurrence` — matrice Jaccard inter-classes au niveau document.
11
- - :mod:`image_predictive` — score de complexité paléographique (poids éditoriaux).
12
-
13
- Rétrocompat
14
- -----------
15
- Les imports historiques ``from picarones.core.taxonomy_intra_doc import
16
- ...`` continuent à fonctionner via des fichiers-shims laissés à
17
- l'ancien emplacement.
18
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
picarones/extras/governance/__init__.py DELETED
@@ -1,8 +0,0 @@
1
- """Gouvernance préventive pour modules contribués externes.
2
-
3
- Aujourd'hui Picarones n'a pas encore de modules tiers contribués par
4
- des utilisateurs externes. Le module ``module_policy`` ici est livré
5
- en avance pour préparer la phase d'ouverture (lointaine).
6
-
7
- Sera réintégré au Cercle 2 si/quand 5+ modules tiers sont publiés.
8
- """
 
 
 
 
 
 
 
 
 
picarones/extras/historical/__init__.py DELETED
@@ -1,30 +0,0 @@
1
- """Métriques philologiques pour documents historiques (Cercle 3).
2
-
3
- Modules orientés cas d'usage patrimoniaux par période :
4
-
5
- - :mod:`unicode_blocks` — précision par bloc Unicode (toutes périodes)
6
- - :mod:`abbreviations` — score d'expansion d'abréviations (médiéval)
7
- - :mod:`mufi` — couverture MUFI v4.0 (médiéval, PUA)
8
- - :mod:`early_modern_typography` — fl, fi, ſ, ã, &, ı (XVIᵉ-XVIIIᵉ siècles)
9
- - :mod:`modern_archives` — Mme/Mlle/°/†/₶ (XIXᵉ-XXᵉ siècles)
10
- - :mod:`roman_numerals` — numéraux romains (toutes périodes)
11
- - :mod:`lexical_modernization` — top tokens GT modernisés par le moteur
12
- - :mod:`philological_runner` — orchestration adaptive des 6 modules
13
-
14
- Utilité
15
- -------
16
- Ces métriques répondent à la question éditoriale *« quels caractères
17
- historiques ce moteur restitue-t-il fidèlement ? »*. Elles ne
18
- participent pas à la décision « peut-on déployer ce moteur en prod ? »
19
- quand le corpus est moderne (les modules retournent ``None`` via
20
- adaptive masking sur un texte sans signal philologique).
21
-
22
- Plugin séparable
23
- ----------------
24
- Distribué via l'extra pip ``picarones[historical]``. Les imports
25
- historiques ``from picarones.core.unicode_blocks import ...`` restent
26
- fonctionnels via des fichiers-shims dans :mod:`picarones.core`.
27
-
28
- Phase B du chantier de refonte en 3 cercles — voir
29
- :doc:`docs/architecture-cercles.md`.
30
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
picarones/{extras/historical → measurements}/abbreviations.py RENAMED
File without changes
picarones/{extras/historical → measurements}/early_modern_typography.py RENAMED
File without changes
picarones/{extras/academic → measurements}/image_predictive.py RENAMED
File without changes
picarones/{extras/historical → measurements}/lexical_modernization.py RENAMED
File without changes
picarones/{extras/historical → measurements}/modern_archives.py RENAMED
File without changes
picarones/{extras/governance → measurements}/module_policy.py RENAMED
File without changes
picarones/{extras/historical → measurements}/mufi.py RENAMED
File without changes
picarones/{extras/historical → measurements}/philological_runner.py RENAMED
@@ -30,12 +30,12 @@ from __future__ import annotations
30
  import logging
31
  from typing import Optional
32
 
33
- from picarones.core.abbreviations import compute_abbreviation_metrics
34
- from picarones.core.early_modern_typography import compute_early_modern_metrics
35
- from picarones.core.modern_archives import compute_modern_archives_metrics
36
- from picarones.core.mufi import compute_mufi_coverage
37
- from picarones.core.roman_numerals import compute_roman_numeral_metrics
38
- from picarones.core.unicode_blocks import compute_unicode_block_accuracy
39
 
40
  logger = logging.getLogger(__name__)
41
 
@@ -296,7 +296,7 @@ def _aggregate_modern_archives(per_doc: list[dict]) -> dict:
296
 
297
 
298
  def _aggregate_roman_numerals(per_doc: list[dict]) -> dict:
299
- from picarones.core.roman_numerals import ALL_STATUSES, VALUE_PRESERVING_STATUSES
300
 
301
  n_total = 0
302
  per_status: dict[str, int] = {s: 0 for s in ALL_STATUSES}
 
30
  import logging
31
  from typing import Optional
32
 
33
+ from picarones.measurements.abbreviations import compute_abbreviation_metrics
34
+ from picarones.measurements.early_modern_typography import compute_early_modern_metrics
35
+ from picarones.measurements.modern_archives import compute_modern_archives_metrics
36
+ from picarones.measurements.mufi import compute_mufi_coverage
37
+ from picarones.measurements.roman_numerals import compute_roman_numeral_metrics
38
+ from picarones.measurements.unicode_blocks import compute_unicode_block_accuracy
39
 
40
  logger = logging.getLogger(__name__)
41
 
 
296
 
297
 
298
  def _aggregate_roman_numerals(per_doc: list[dict]) -> dict:
299
+ from picarones.measurements.roman_numerals import ALL_STATUSES, VALUE_PRESERVING_STATUSES
300
 
301
  n_total = 0
302
  per_status: dict[str, int] = {s: 0 for s in ALL_STATUSES}
picarones/{extras/historical → measurements}/roman_numerals.py RENAMED
File without changes
picarones/{extras/academic → measurements}/taxonomy_cooccurrence.py RENAMED
File without changes
picarones/{extras/academic → measurements}/taxonomy_intra_doc.py RENAMED
@@ -42,7 +42,7 @@ import logging
42
  import unicodedata
43
  from typing import Optional
44
 
45
- from picarones.core.taxonomy import (
46
  ERROR_CLASSES,
47
  _is_abbreviation_error,
48
  _is_diacritic_error,
 
42
  import unicodedata
43
  from typing import Optional
44
 
45
+ from picarones.measurements.taxonomy import (
46
  ERROR_CLASSES,
47
  _is_abbreviation_error,
48
  _is_diacritic_error,
picarones/{extras/historical → measurements}/unicode_blocks.py RENAMED
File without changes
tests/test_phaseA_migration.py DELETED
@@ -1,318 +0,0 @@
1
- """Tests de la phase A — refonte en 3 cercles (post-chantier 6).
2
-
3
- Couvre :
4
-
5
- - 4 modules `core/` déplacés vers `extras/academic/` ou
6
- `extras/governance/` avec shims rétrocompat.
7
- - 4 renderers `report/` déplacés vers `extras/render/` avec shims.
8
- - Identité préservée : ``shim.X is new_location.X`` (pas de duplication
9
- ni de redéfinition).
10
- - Hygiène anti-verdict : 5 phrases reformulées dans les templates
11
- narratifs et l'i18n du rapport.
12
- - Document `docs/architecture-cercles.md` présent et complet.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- from pathlib import Path
18
-
19
- import pytest
20
-
21
-
22
- # ──────────────────────────────────────────────────────────────────────────
23
- # 1. Modules déplacés vers extras/ — rétrocompat des imports historiques
24
- # ──────────────────────────────────────────────────────────────────────────
25
-
26
-
27
- class TestRetrocompatHistoricalImports:
28
- """Les imports `from picarones.core.X` doivent continuer à fonctionner
29
- après le déplacement vers `picarones.extras.*`."""
30
-
31
- @pytest.mark.parametrize("module_path, attribute", [
32
- ("picarones.core.taxonomy_intra_doc", "compute_taxonomy_position_heatmap"),
33
- ("picarones.core.taxonomy_cooccurrence", "compute_taxonomy_cooccurrence"),
34
- ("picarones.core.image_predictive", "compute_paleographic_complexity"),
35
- ("picarones.core.image_predictive", "compute_corpus_homogeneity"),
36
- ("picarones.core.image_predictive", "aggregate_corpus_predictive"),
37
- ("picarones.core.module_policy", "ModuleManifest"),
38
- ("picarones.core.module_policy", "validate_manifest"),
39
- ("picarones.core.module_policy", "audit_module"),
40
- ])
41
- def test_core_alias_still_works(self, module_path: str, attribute: str):
42
- import importlib
43
- mod = importlib.import_module(module_path)
44
- assert hasattr(mod, attribute), (
45
- f"{module_path}.{attribute} a disparu après la phase A — "
46
- "le shim rétrocompat est cassé"
47
- )
48
-
49
- @pytest.mark.parametrize("module_path, attribute", [
50
- ("picarones.report.taxonomy_intra_doc_render", "build_taxonomy_intra_doc_html"),
51
- ("picarones.report.taxonomy_cooccurrence_render", "build_taxonomy_cooccurrence_html"),
52
- ("picarones.report.image_predictive_render", "build_image_predictive_html"),
53
- ("picarones.report.module_audit_render", "build_module_audit_html"),
54
- ])
55
- def test_report_alias_still_works(self, module_path: str, attribute: str):
56
- import importlib
57
- mod = importlib.import_module(module_path)
58
- assert hasattr(mod, attribute)
59
-
60
-
61
- # ──────────────────────────────────────────────────────────────────────────
62
- # 2. Modules accessibles via leur nouveau chemin extras/
63
- # ──────────────────────────────────────────────────────────────────────────
64
-
65
-
66
- class TestNewExtrasImports:
67
- @pytest.mark.parametrize("new_path, attribute", [
68
- ("picarones.extras.academic.taxonomy_intra_doc", "compute_taxonomy_position_heatmap"),
69
- ("picarones.extras.academic.taxonomy_cooccurrence", "compute_taxonomy_cooccurrence"),
70
- ("picarones.extras.academic.image_predictive", "aggregate_corpus_predictive"),
71
- ("picarones.extras.governance.module_policy", "ModuleManifest"),
72
- ("picarones.extras.render.taxonomy_intra_doc_render", "build_taxonomy_intra_doc_html"),
73
- ("picarones.extras.render.taxonomy_cooccurrence_render", "build_taxonomy_cooccurrence_html"),
74
- ("picarones.extras.render.image_predictive_render", "build_image_predictive_html"),
75
- ("picarones.extras.render.module_audit_render", "build_module_audit_html"),
76
- ])
77
- def test_extras_path_works(self, new_path: str, attribute: str):
78
- import importlib
79
- mod = importlib.import_module(new_path)
80
- assert hasattr(mod, attribute)
81
-
82
-
83
- # ──────────────────────────────────────────────────────────────────────────
84
- # 3. Identité préservée — pas de redéfinition par le shim
85
- # ──────────────────────────────────────────────────────────────────────────
86
-
87
-
88
- class TestIdentityThroughShim:
89
- """Le shim doit réexporter la fonction du nouveau chemin, pas la
90
- redéfinir. Sinon une métrique serait calculée différemment selon
91
- le chemin d'import."""
92
-
93
- def test_taxonomy_intra_doc_identity(self):
94
- from picarones.core.taxonomy_intra_doc import (
95
- compute_taxonomy_position_heatmap as via_old,
96
- )
97
- from picarones.extras.academic.taxonomy_intra_doc import (
98
- compute_taxonomy_position_heatmap as via_new,
99
- )
100
- assert via_old is via_new
101
-
102
- def test_image_predictive_identity(self):
103
- from picarones.core.image_predictive import (
104
- aggregate_corpus_predictive as via_old,
105
- )
106
- from picarones.extras.academic.image_predictive import (
107
- aggregate_corpus_predictive as via_new,
108
- )
109
- assert via_old is via_new
110
-
111
- def test_module_policy_identity(self):
112
- from picarones.core.module_policy import ModuleManifest as via_old
113
- from picarones.extras.governance.module_policy import (
114
- ModuleManifest as via_new,
115
- )
116
- assert via_old is via_new
117
-
118
- def test_renderer_identity(self):
119
- from picarones.report.taxonomy_intra_doc_render import (
120
- build_taxonomy_intra_doc_html as via_old,
121
- )
122
- from picarones.extras.render.taxonomy_intra_doc_render import (
123
- build_taxonomy_intra_doc_html as via_new,
124
- )
125
- assert via_old is via_new
126
-
127
-
128
- # ──────────────────────────────────────────────────────────────────────────
129
- # 4. Vues du chantier 3 — toujours fonctionnelles
130
- # ──────────────────────────────────────────────────────────────────────────
131
-
132
-
133
- class TestChantier3ViewsStillWork:
134
- """Les 5 vues du chantier 3 importent (sous-section opt-in) les
135
- modules déplacés. Vérifier qu'elles tournent encore après la
136
- migration."""
137
-
138
- def test_views_import(self):
139
- from picarones.report.views import (
140
- build_advanced_taxonomy_view_html,
141
- build_diagnostics_view_html,
142
- build_economics_view_html,
143
- build_pipeline_view_html,
144
- build_robustness_view_html,
145
- )
146
- assert callable(build_advanced_taxonomy_view_html)
147
- assert callable(build_diagnostics_view_html)
148
- assert callable(build_economics_view_html)
149
- assert callable(build_pipeline_view_html)
150
- assert callable(build_robustness_view_html)
151
-
152
- def test_advanced_taxonomy_with_intra_doc_data(self):
153
- """La vue advanced_taxonomy accepte des données opt-in
154
- ``intra_doc`` dont le calcul vient désormais de
155
- ``picarones.extras.academic``."""
156
- from picarones.extras.academic.taxonomy_intra_doc import (
157
- compute_taxonomy_position_heatmap,
158
- )
159
- from picarones.report.views import build_advanced_taxonomy_view_html
160
-
161
- # Calcul d'une heatmap minimaliste
162
- result = compute_taxonomy_position_heatmap(
163
- "abc def ghi", "abx def ghi", n_bins=3,
164
- )
165
- # La vue doit pouvoir composer sans crasher quand on lui passe
166
- # ces données opt-in
167
- report_data = {"engines": [
168
- {"name": "tess", "cer": 0.05,
169
- "aggregated_taxonomy": {"class_distribution": {"x": 5}}},
170
- {"name": "pero", "cer": 0.08,
171
- "aggregated_taxonomy": {"class_distribution": {"x": 8}}},
172
- ]}
173
- html = build_advanced_taxonomy_view_html(
174
- report_data, {}, intra_doc=result,
175
- )
176
- # Pas de crash + au moins du contenu (comparison + intra_doc)
177
- assert isinstance(html, str)
178
-
179
-
180
- # ──────────────────────────────────────────────────────────────────────────
181
- # 5. Hygiène anti-verdict — phrases reformulées
182
- # ──────────────────────────────────────────────────────────────────────────
183
-
184
-
185
- class TestAntiVerdictHygiene:
186
- """Les 5 phrases identifiées comme prescriptives ont été reformulées
187
- factuellement. Tests anti-régression."""
188
-
189
- @pytest.fixture
190
- def fr_templates(self) -> str:
191
- path = (Path(__file__).parent.parent
192
- / "picarones" / "core" / "narrative" / "templates" / "fr.yaml")
193
- return path.read_text(encoding="utf-8")
194
-
195
- @pytest.fixture
196
- def en_templates(self) -> str:
197
- path = (Path(__file__).parent.parent
198
- / "picarones" / "core" / "narrative" / "templates" / "en.yaml")
199
- return path.read_text(encoding="utf-8")
200
-
201
- @pytest.fixture
202
- def fr_i18n(self) -> str:
203
- path = (Path(__file__).parent.parent
204
- / "picarones" / "report" / "i18n" / "fr.json")
205
- return path.read_text(encoding="utf-8")
206
-
207
- @pytest.fixture
208
- def en_i18n(self) -> str:
209
- path = (Path(__file__).parent.parent
210
- / "picarones" / "report" / "i18n" / "en.json")
211
- return path.read_text(encoding="utf-8")
212
-
213
- def test_stratum_winner_no_dominate(self, fr_templates, en_templates):
214
- """`stratum_winner` ne dit plus « domine nettement » /
215
- « clearly dominates ». Phrasage factuel attendu."""
216
- assert "domine\n nettement" not in fr_templates
217
- assert "domine nettement" not in fr_templates
218
- assert "clearly\n dominates" not in en_templates
219
- assert "clearly dominates" not in en_templates
220
- # Confirmation présence du nouveau phrasage factuel
221
- assert "le CER le plus bas" in fr_templates
222
- assert "the lowest CER" in en_templates
223
-
224
- def test_confidence_warning_no_fragile(self, fr_templates, en_templates):
225
- """`confidence_warning` ne dit plus « fragile » mais
226
- « incertitude statistique élevée »."""
227
- assert "Classement fragile" not in fr_templates
228
- assert "Ranking is fragile" not in en_templates
229
- assert "Incertitude statistique" in fr_templates
230
- assert "High statistical uncertainty" in en_templates
231
-
232
- def test_gini_no_ideal(self, fr_i18n, en_i18n):
233
- """`gini_cer_ideal` et `gini_cer_note` n'utilisent plus
234
- « idéal » / « ideal » mais « lecture » / « reading »."""
235
- assert "\"gini_cer_ideal\": \"— idéal" not in fr_i18n
236
- assert "\"gini_cer_ideal\": \"— ideal" not in en_i18n
237
- # Confirmer le nouveau phrasage
238
- assert "lecture : bas-gauche" in fr_i18n
239
- assert "reading: bottom-left" in en_i18n
240
-
241
- def test_taxocomp_no_preferable(self, fr_i18n, en_i18n):
242
- """`taxocomp_note` ne dit plus « préférable » / « preferable »."""
243
- assert "préférable pour une édition critique" not in fr_i18n
244
- assert "preferable for a critical edition" not in en_i18n
245
- # Phrasage factuel
246
- assert "tend à produire des erreurs plus facilement" in fr_i18n
247
- assert "tends to produce errors more easily" in en_i18n
248
-
249
-
250
- # ──────────────────────────────────────────────────────────────────────────
251
- # 6. Document docs/architecture-cercles.md présent et complet
252
- # ──────────────────────────────────────────────────────────────────────────
253
-
254
-
255
- class TestArchitectureCerclesDoc:
256
- @pytest.fixture
257
- def doc(self) -> str:
258
- path = (Path(__file__).parent.parent / "docs" / "architecture-cercles.md")
259
- return path.read_text(encoding="utf-8")
260
-
261
- def test_doc_exists(self, doc):
262
- assert len(doc) > 1000
263
-
264
- def test_doc_describes_three_circles(self, doc):
265
- assert "Cercle 1" in doc
266
- assert "Cercle 2" in doc
267
- assert "Cercle 3" in doc
268
- assert "Noyau invariant" in doc or "noyau invariant" in doc
269
- assert "Plugins" in doc or "plugins" in doc
270
-
271
- def test_doc_assigns_specific_modules(self, doc):
272
- """Le document doit lister explicitement les modules de chaque cercle."""
273
- # Cercle 1 — quelques noms
274
- for name in ["corpus.py", "modules.py", "runner.py",
275
- "metric_registry.py", "alto_metrics.py"]:
276
- assert name in doc, f"{name} doit être listé dans le doc"
277
- # Cercle 3 — modules déplacés en phase A
278
- for name in ["taxonomy_intra_doc", "image_predictive",
279
- "module_policy"]:
280
- assert name in doc, f"{name} doit être listé dans le doc"
281
-
282
- def test_doc_mentions_extras_path(self, doc):
283
- """Le doc explique que les Cercle 3 vivent dans `extras/`."""
284
- assert "extras/academic" in doc
285
- assert "extras/governance" in doc
286
- assert "extras/render" in doc
287
-
288
-
289
- # ──────────────────────────────────────────────────────────────────────────
290
- # 7. Modules originaux ne contiennent plus de logique métier
291
- # ──────────────────────────────────────────────────────────────────────────
292
-
293
-
294
- class TestOriginalsAreShims:
295
- """Vérifie que les fichiers laissés à l'ancien emplacement sont
296
- bien des shims minces, pas des copies de la logique."""
297
-
298
- @pytest.mark.parametrize("path", [
299
- "picarones/core/taxonomy_intra_doc.py",
300
- "picarones/core/taxonomy_cooccurrence.py",
301
- "picarones/core/image_predictive.py",
302
- "picarones/core/module_policy.py",
303
- "picarones/report/taxonomy_intra_doc_render.py",
304
- "picarones/report/taxonomy_cooccurrence_render.py",
305
- "picarones/report/image_predictive_render.py",
306
- "picarones/report/module_audit_render.py",
307
- ])
308
- def test_is_thin_shim(self, path):
309
- repo_root = Path(__file__).parent.parent
310
- content = (repo_root / path).read_text(encoding="utf-8")
311
- # Un shim < 30 lignes (juste docstring + 2 imports + __all__)
312
- n_lines = len([line for line in content.splitlines() if line.strip()])
313
- assert n_lines < 30, (
314
- f"{path} fait {n_lines} lignes — devrait être un shim mince "
315
- "(import + réexport, pas de logique métier)"
316
- )
317
- # Doit contenir l'indication du déplacement
318
- assert "déplacé" in content or "extras" in content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_phaseB_migration.py DELETED
@@ -1,249 +0,0 @@
1
- """Tests de la phase B — extras/historical/ (philologique vers Cercle 3).
2
-
3
- Couvre :
4
-
5
- - 8 modules philologiques (Cercle 3) déplacés vers `extras/historical/`.
6
- - 2 renderers correspondants déplacés vers `extras/render/`.
7
- - Identité préservée à travers les shims (test ``is``).
8
- - Intégration : `philological_runner` orchestre toujours les 6 modules
9
- même après déplacement.
10
- - Dépendance Cercle 2 → Cercle 3 (`numerical_sequences` →
11
- `roman_numerals`) continue de fonctionner via shim.
12
- - pyproject.toml déclare `[historical]` comme extra documentaire.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- from pathlib import Path
18
-
19
- import pytest
20
-
21
-
22
- # ──────────────────────────────────────────────────────────────────────────
23
- # 1. Modules historiques accessibles via shims (rétrocompat)
24
- # ──────────────────────────────────────────────────────────────────────────
25
-
26
-
27
- class TestPhilologicalRetrocompat:
28
- @pytest.mark.parametrize("module_path, attribute", [
29
- ("picarones.core.unicode_blocks", "compute_unicode_block_accuracy"),
30
- ("picarones.core.abbreviations", "compute_abbreviation_metrics"),
31
- ("picarones.core.mufi", "compute_mufi_coverage"),
32
- ("picarones.core.early_modern_typography", "compute_early_modern_metrics"),
33
- ("picarones.core.modern_archives", "compute_modern_archives_metrics"),
34
- ("picarones.core.roman_numerals", "compute_roman_numeral_metrics"),
35
- ("picarones.core.lexical_modernization", "compute_lexical_modernization"),
36
- ("picarones.core.philological_runner", "compute_philological_metrics"),
37
- ("picarones.core.philological_runner", "aggregate_philological_metrics"),
38
- ])
39
- def test_core_alias_still_works(self, module_path: str, attribute: str):
40
- import importlib
41
- mod = importlib.import_module(module_path)
42
- assert hasattr(mod, attribute), (
43
- f"{module_path}.{attribute} a disparu après la phase B"
44
- )
45
-
46
- @pytest.mark.parametrize("module_path, attribute", [
47
- ("picarones.report.philological_render", "build_philological_profile_html"),
48
- ("picarones.report.lexical_modernization_render",
49
- "build_lexical_modernization_html"),
50
- ])
51
- def test_render_alias_still_works(self, module_path: str, attribute: str):
52
- import importlib
53
- mod = importlib.import_module(module_path)
54
- assert hasattr(mod, attribute)
55
-
56
-
57
- # ──────────────────────────────────────────────────────────────────────────
58
- # 2. Modules accessibles via leur nouveau chemin extras/historical/
59
- # ──────────────────────────────────────────────────────────────────────────
60
-
61
-
62
- class TestNewHistoricalImports:
63
- @pytest.mark.parametrize("new_path, attribute", [
64
- ("picarones.extras.historical.unicode_blocks",
65
- "compute_unicode_block_accuracy"),
66
- ("picarones.extras.historical.abbreviations",
67
- "compute_abbreviation_metrics"),
68
- ("picarones.extras.historical.mufi", "compute_mufi_coverage"),
69
- ("picarones.extras.historical.early_modern_typography",
70
- "compute_early_modern_metrics"),
71
- ("picarones.extras.historical.modern_archives",
72
- "compute_modern_archives_metrics"),
73
- ("picarones.extras.historical.roman_numerals",
74
- "compute_roman_numeral_metrics"),
75
- ("picarones.extras.historical.lexical_modernization",
76
- "compute_lexical_modernization"),
77
- ("picarones.extras.historical.philological_runner",
78
- "compute_philological_metrics"),
79
- ("picarones.extras.render.philological_render",
80
- "build_philological_profile_html"),
81
- ("picarones.extras.render.lexical_modernization_render",
82
- "build_lexical_modernization_html"),
83
- ])
84
- def test_extras_path_works(self, new_path: str, attribute: str):
85
- import importlib
86
- mod = importlib.import_module(new_path)
87
- assert hasattr(mod, attribute)
88
-
89
-
90
- # ──────────────────────────────────────────────────────────────────────────
91
- # 3. Identité préservée (shim et nouveau chemin = même fonction)
92
- # ──────────────────────────────────────────────────────────────────────────
93
-
94
-
95
- class TestIdentityThroughShim:
96
- def test_unicode_blocks_identity(self):
97
- from picarones.core.unicode_blocks import (
98
- compute_unicode_block_accuracy as via_old,
99
- )
100
- from picarones.extras.historical.unicode_blocks import (
101
- compute_unicode_block_accuracy as via_new,
102
- )
103
- assert via_old is via_new
104
-
105
- def test_philological_runner_identity(self):
106
- from picarones.core.philological_runner import (
107
- compute_philological_metrics as via_old,
108
- )
109
- from picarones.extras.historical.philological_runner import (
110
- compute_philological_metrics as via_new,
111
- )
112
- assert via_old is via_new
113
-
114
- def test_renderer_identity(self):
115
- from picarones.report.philological_render import (
116
- build_philological_profile_html as via_old,
117
- )
118
- from picarones.extras.render.philological_render import (
119
- build_philological_profile_html as via_new,
120
- )
121
- assert via_old is via_new
122
-
123
-
124
- # ──────────────────────────────────────────────────────────────────────────
125
- # 4. Intégration : philological_runner orchestre toujours les 6 modules
126
- # ──────────────────────────────────────────────────────────────────────────
127
-
128
-
129
- class TestPhilologicalRunnerIntegration:
130
- """Le runner philologique appelle les 6 modules
131
- philologiques. Vérifie que cette chaîne fonctionne après le
132
- déplacement (les imports internes traversent les shims)."""
133
-
134
- def test_runner_returns_dict_or_none(self):
135
- from picarones.core.philological_runner import (
136
- compute_philological_metrics,
137
- )
138
- # Texte sans signal philologique → None par adaptive masking
139
- result = compute_philological_metrics(
140
- "Bonjour le monde", "Bonjour le monde",
141
- )
142
- # None acceptable (texte ASCII pur sans aucun marqueur)
143
- # OU dict vide (signal nul partout)
144
- assert result is None or isinstance(result, dict)
145
-
146
- def test_runner_with_medieval_text(self):
147
- """Texte médiéval avec abréviations + numéraux romains : on
148
- s'attend à au moins un module qui détecte du signal."""
149
- from picarones.core.philological_runner import (
150
- compute_philological_metrics,
151
- )
152
- # ⁊ = symbole d'abréviation Capelli ; XIV = numéral romain ; ſ = long s
153
- ref = "⁊ par leſ XIV. fontoyers"
154
- hyp = "et par les XIV. fontoyers"
155
- result = compute_philological_metrics(ref, hyp)
156
- # Au moins un module doit avoir détecté du signal
157
- # (abbreviations OU early_modern OU roman_numerals)
158
- assert result is not None
159
- assert isinstance(result, dict)
160
- assert len(result) >= 1
161
-
162
-
163
- # ──────────────────────────────────────────────────────────────────────────
164
- # 5. Dépendance Cercle 2 → Cercle 3 fonctionne via shim
165
- # ──────────────────────────────────────────────────────────────────────────
166
-
167
-
168
- class TestCercle2DependsOnCercle3ViaShim:
169
- """``picarones.core.numerical_sequences`` (Cercle 2,
170
- measurements/) importe ``roman_numerals`` (Cercle 3, extras/).
171
- Cette dépendance traverse le shim — elle continue à fonctionner."""
172
-
173
- def test_numerical_sequences_uses_roman_numerals(self):
174
- from picarones.core.numerical_sequences import (
175
- compute_numerical_sequence_metrics,
176
- )
177
- # Texte avec numéral romain
178
- result = compute_numerical_sequence_metrics(
179
- "Le roi Louis XIV régna jusqu'en 1715",
180
- "Le roi Louis XIV régna jusqu'en 1715",
181
- )
182
- # Le score strict global doit refléter au moins la détection
183
- # du romain et de la date
184
- assert isinstance(result, dict)
185
- assert result.get("global_strict_score") is not None
186
- assert result.get("global_strict_score") >= 0.5
187
-
188
-
189
- # ──────────────────────────────────────────────────────────────────────────
190
- # 6. pyproject.toml déclare l'extra [historical]
191
- # ──────────────────────────────────────────────────────────────────────────
192
-
193
-
194
- class TestPyprojectExtra:
195
- def test_historical_extra_declared(self):
196
- path = Path(__file__).parent.parent / "pyproject.toml"
197
- content = path.read_text(encoding="utf-8")
198
- # L'extra [historical] doit être déclaré, même vide
199
- assert "historical = []" in content or 'historical = [' in content
200
- # Documentation de l'intention présente
201
- assert "extras/historical" in content
202
- assert "Cercle 3" in content
203
-
204
-
205
- # ──────────────────────────────────────────────────────────────────────────
206
- # 7. Hooks builtin enregistrés conditionnels (philological + lexical)
207
- # ──────────────────────────────────────────────────────────────────────────
208
-
209
-
210
- class TestBuiltinHooksStillRegisterPhilological:
211
- """Les hooks ``philological`` et ``lexical_modernization``
212
- s'enregistrent au chargement de :mod:`picarones.core.builtin_hooks`
213
- via les imports qui traversent les shims (``from
214
- picarones.core.philological_runner import ...``)."""
215
-
216
- def test_philological_hook_registered(self):
217
- # L'import déclenche l'enregistrement
218
- import picarones.core.builtin_hooks # noqa: F401
219
- from picarones.core.metric_hooks import _all_document_hook_names
220
-
221
- assert "philological" in _all_document_hook_names()
222
-
223
-
224
- # ──────────────────────────────────────────────────────────────────────────
225
- # 8. Modules originaux sont des shims minces
226
- # ──────────────────────────────────────────────────────────────────────────
227
-
228
-
229
- class TestOriginalsAreShims:
230
- @pytest.mark.parametrize("path", [
231
- "picarones/core/unicode_blocks.py",
232
- "picarones/core/abbreviations.py",
233
- "picarones/core/mufi.py",
234
- "picarones/core/early_modern_typography.py",
235
- "picarones/core/modern_archives.py",
236
- "picarones/core/roman_numerals.py",
237
- "picarones/core/lexical_modernization.py",
238
- "picarones/core/philological_runner.py",
239
- "picarones/report/philological_render.py",
240
- "picarones/report/lexical_modernization_render.py",
241
- ])
242
- def test_is_thin_shim(self, path):
243
- repo_root = Path(__file__).parent.parent
244
- content = (repo_root / path).read_text(encoding="utf-8")
245
- n_lines = len([line for line in content.splitlines() if line.strip()])
246
- assert n_lines < 30, (
247
- f"{path} fait {n_lines} lignes — devrait être un shim mince"
248
- )
249
- assert "déplacé" in content or "extras" in content