Spaces:
Running
refactor(measurements): promouvoir modules philologiques/académiques/governance depuis extras/
Browse filesLes 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 +0 -18
- picarones/extras/governance/__init__.py +0 -8
- picarones/extras/historical/__init__.py +0 -30
- picarones/{extras/historical → measurements}/abbreviations.py +0 -0
- picarones/{extras/historical → measurements}/early_modern_typography.py +0 -0
- picarones/{extras/academic → measurements}/image_predictive.py +0 -0
- picarones/{extras/historical → measurements}/lexical_modernization.py +0 -0
- picarones/{extras/historical → measurements}/modern_archives.py +0 -0
- picarones/{extras/governance → measurements}/module_policy.py +0 -0
- picarones/{extras/historical → measurements}/mufi.py +0 -0
- picarones/{extras/historical → measurements}/philological_runner.py +7 -7
- picarones/{extras/historical → measurements}/roman_numerals.py +0 -0
- picarones/{extras/academic → measurements}/taxonomy_cooccurrence.py +0 -0
- picarones/{extras/academic → measurements}/taxonomy_intra_doc.py +1 -1
- picarones/{extras/historical → measurements}/unicode_blocks.py +0 -0
- tests/test_phaseA_migration.py +0 -318
- tests/test_phaseB_migration.py +0 -249
|
@@ -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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -30,12 +30,12 @@ from __future__ import annotations
|
|
| 30 |
import logging
|
| 31 |
from typing import Optional
|
| 32 |
|
| 33 |
-
from picarones.
|
| 34 |
-
from picarones.
|
| 35 |
-
from picarones.
|
| 36 |
-
from picarones.
|
| 37 |
-
from picarones.
|
| 38 |
-
from picarones.
|
| 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.
|
| 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}
|
|
File without changes
|
|
File without changes
|
|
@@ -42,7 +42,7 @@ import logging
|
|
| 42 |
import unicodedata
|
| 43 |
from typing import Optional
|
| 44 |
|
| 45 |
-
from picarones.
|
| 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,
|
|
File without changes
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|