227 lines
9.3 KiB
Python
227 lines
9.3 KiB
Python
"""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 « 5–6 » 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
|