Files
veye-lalwa/tests/test_citations.py
T

227 lines
9.3 KiB
Python
Raw Normal View History

"""Blocs sourcés, notes, bibliographie et résolution des marqueurs."""
from __future__ import annotations
from datetime import date
import pytest
from pipeline import chemins
from pipeline.modeles import Confiance
from pipeline.parseurs import bibliographie, blocs_claim, citations, notes_bas_page
from pipeline.parseurs.markdown import extraire_marqueurs
# ─────────────────────────────────────────────────────────────────────────────
# Blocs sourcés — les quatre dialectes du corpus
# ─────────────────────────────────────────────────────────────────────────────
DIALECTE_A = """\
Claim: La loi n° 2026-350 est publiée au JORF du 10 mai 2026 [^1^].
Source: Légifrance
URL: https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000054049780
Date: 10 mai 2026
Excerpt: « visant à garantir le droit de visite »
Confidence: high
"""
DIALECTE_B = """\
**Claim 3 :** Le gouvernement a élargi le texte par lettre rectificative [^3^]
Source: DHnet / AFP
URL: https://www.dhnet.be/exemple
Date: 1er juillet 2026
Confidence: high
"""
DIALECTE_C = """\
**Claim 1.2** — Le Conseil d'État confirme que le texte comporte 23 articles.
- **Source** : Conseil d'État
- **URL** : https://www.conseil-etat.fr/avis ; https://www.senat.fr/rap/exemple.html
- **Date** : 2 avril 2026
- **Confidence** : Haute
"""
DIALECTE_PHASE5 = """\
**Claim 1.1 (décisif, source primaire) : le texte fixe l'AFD à 500 €.**
- **Source :** Assemblée nationale, TEXTE ADOPTÉ n° 340
- **URL :** https://www.assemblee-nationale.fr/dyn/17/textes/l17t0340_texte-adopte-provisoire
- **Date :** 21/07/2026
- **Confidence : très élevée**
"""
@pytest.mark.parametrize(
("source", "urls_attendues"),
[
(DIALECTE_A, 1),
(DIALECTE_B, 1),
(DIALECTE_C, 2), # deux URLs séparées par « ; » sur la même ligne
(DIALECTE_PHASE5, 1),
],
)
def test_les_quatre_dialectes_sont_lus(source: str, urls_attendues: int) -> None:
(bloc,) = blocs_claim.parser(source, "dimXX")
assert len(bloc.urls) == urls_attendues
assert bloc.confiance is Confiance.HIGH
def test_dialecte_a_complet() -> None:
(bloc,) = blocs_claim.parser(DIALECTE_A, "dim01")
assert bloc.marqueurs == ["1"]
assert bloc.editeur == "Légifrance"
assert bloc.date_publication == date(2026, 5, 10)
assert bloc.extrait == "visant à garantir le droit de visite"
(source,) = bloc.en_sources()
assert source.tier == "T1" # legifrance.gouv.fr reconnu comme primaire
assert source.marqueur == "dim01-1"
def test_phase5_sans_la_parenthese_ne_serait_pas_lu() -> None:
"""La forme « **Claim 1.1 (décisif…) : … » est propre à la validation."""
blocs = blocs_claim.parser(DIALECTE_PHASE5, "phase5")
assert len(blocs) == 1
assert blocs[0].confiance is Confiance.HIGH
@pytest.mark.parametrize(
("valeur", "attendu"),
[
("high", Confiance.HIGH),
("Haute", Confiance.HIGH),
("très élevée", Confiance.HIGH),
("élevée", Confiance.HIGH),
("medium", Confiance.MEDIUM),
("medium-high", Confiance.MEDIUM),
("low", Confiance.LOW),
("valeur inconnue", Confiance.MEDIUM),
],
)
def test_lecture_de_la_confiance(valeur: str, attendu: Confiance) -> None:
assert blocs_claim.lire_confiance(valeur) is attendu
def test_tous_les_rapports_de_dimension_rendent_des_blocs() -> None:
"""Aucune dimension ne doit être muette : ce serait un dialecte manqué."""
for numero in range(1, 13):
markdown = chemins.dimension(numero).read_text(encoding="utf-8")
blocs = blocs_claim.parser(markdown, f"dim{numero:02d}")
assert blocs, f"dim{numero:02d} n'a produit aucun bloc"
# ─────────────────────────────────────────────────────────────────────────────
# Notes de bas de page
# ─────────────────────────────────────────────────────────────────────────────
def test_note_avec_url() -> None:
ligne = "[^1141^] senat.fr — Texte n° 890 (09/07/2026) — https://www.senat.fr/leg/pjl25-890.html"
(note,) = notes_bas_page.parser(ligne, "dim09")
assert note.numero == "1141"
assert note.url == "https://www.senat.fr/leg/pjl25-890.html"
assert note.editeur == "senat.fr"
assert note.date_publication == date(2026, 7, 9)
assert note.en_source() is not None
def test_note_sans_url_reste_sans_source() -> None:
(note,) = notes_bas_page.parser("[^44^] diplomatie.gouv.fr, 16/04/2026.", "dim01")
assert note.url is None
assert note.en_source() is None
assert note.texte # le libellé est conservé
def test_url_legifrance_reconstruite_depuis_un_identifiant_jorf() -> None:
"""Le schéma d'URL de Légifrance est fixe : le reconstruire n'invente rien."""
(note,) = notes_bas_page.parser(
"[^1^] Légifrance, JORFTEXT000054049780 (loi 2026-350).", "dim01"
)
assert note.url == "https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000054049780"
assert note.url_reconstruite is True
# ─────────────────────────────────────────────────────────────────────────────
# Bibliographie
# ─────────────────────────────────────────────────────────────────────────────
def test_bibliographie_entierement_liee() -> None:
references = bibliographie.parser(chemins.BIBLIOGRAPHIE.read_text(encoding="utf-8"))
assert len(references) == 481
assert all(r.url for r in references), "la bibliographie doit être intégralement liée"
def test_numero_de_loi_translittere() -> None:
(reference,) = bibliographie.parser(
"[1] Aida. Loi ndeg 2026 554 290626 visant a relancer [EB/OL]. 29 juin 2026. https://x.fr/a"
)
assert "2026-554" in reference.numeros_cites
def test_les_plages_de_pages_ne_font_pas_des_numeros_de_loi() -> None:
(reference,) = bibliographie.parser(
"[2] Sénat. Programmation militaire 2024-2030 [EB/OL]. 2026. https://senat.fr/x"
)
assert "2024-2030" not in reference.numeros_cites
def test_cotes_de_documents_parlementaires() -> None:
assert bibliographie.numeros_de_document("L17b3025 proposition loi") == ["3025"]
assert bibliographie.numeros_de_document("ppl25-691") == ["691"]
assert bibliographie.numeros_de_document("Texte n° 890") == ["890"]
def test_table_de_correspondance_des_prefixes() -> None:
"""Les plages « 56 » doivent être dépliées."""
correspondances = bibliographie.lire_table_de_correspondance(
chemins.BIBLIOGRAPHIE.read_text(encoding="utf-8")
)
assert set(correspondances) >= {f"dim{n:02d}" for n in range(1, 13)} | {"phase5"}
assert 5 in correspondances["dim01"] and 6 in correspondances["dim01"]
# ─────────────────────────────────────────────────────────────────────────────
# Résolution des marqueurs
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def repertoire() -> citations.Repertoire:
return citations.construire()
def test_tous_les_marqueurs_du_rapport_sont_resolus(repertoire) -> None:
"""Aucun marqueur du rapport ne doit rester orphelin."""
repertoire.remettre_a_zero_diagnostic()
marqueurs = {}
for numero in range(11):
for marqueur in extraire_marqueurs(
chemins.chapitre(numero).read_text(encoding="utf-8")
):
marqueurs[marqueur.brut] = marqueur
for marqueur in marqueurs.values():
repertoire.resoudre(marqueur)
assert repertoire.non_resolus == []
assert repertoire.taux_de_resolution == 1.0
# La très grande majorité doit aboutir à une URL, pas seulement à un libellé.
assert len(repertoire.resolus_avec_lien) / len(marqueurs) > 0.90
def test_resolution_d_un_marqueur_de_dimension(repertoire) -> None:
source = repertoire.resoudre("dim07-24")
assert source is not None
assert source.url.startswith("http")
assert source.marqueur == "dim07-24"
def test_marqueurs_insight_sans_source_par_construction(repertoire) -> None:
"""La bibliographie indique qu'insight n'a « aucune source primaire propre »."""
repertoire.remettre_a_zero_diagnostic()
assert repertoire.resoudre("insight-5") is None
assert repertoire.non_resolus == []
def test_notes_sans_lien_rapprochees_de_la_bibliographie(repertoire) -> None:
assert repertoire.rapprochements > 0