"""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