Files
veye-lalwa/tests/test_citations.py
Cyber MawonajandClaude Opus 5 c28243cf87 Phase 3 : sources, citations et seed complet — 368 sources, 0 texte orphelin
Parseurs :
- blocs_claim : les quatre dialectes des rapports de dimension. Le cahier des
  charges en postulait un seul ; sans les quatre, dim05 à dim12 étaient perdus,
  soit les dimensions associations, entreprises, migration et calendrier.
- notes_bas_page : 258 notes, dont 104 sans URL. Les identifiants JORFTEXT
  donnent lieu à une URL canonique reconstruite (schéma fixe, rien d'inventé) ;
  les autres restent sans lien plutôt que d'être devinées.
- bibliographie : 481 références, toutes liées. Indexées par numéro de loi,
  par affaire DC et par cote de document parlementaire — cette dernière étant
  la seule identité d'un texte déposé et non examiné.
- chapitres : rattachement phrase par phrase, résumés repris tels quels.

Résolution des citations : 100 % des 332 marqueurs du rapport, 93 % avec un
lien vérifiable. Il a fallu six conventions de numérotation, une par famille de
dimension (marqueur de fin, en-tête de claim, section, rang, note, section.rang).

Trois pièges corrigés, tous couverts par des tests :
- « décision n° 2026-903 DC » lu comme une loi, ce qui volait à la loi 2026-403
  les phrases — et donc les sources — qui parlaient d'elle
- retrait des marqueurs laissant « … dans les DROM . » ; corrigé à la source
  sans toucher aux espaces avant « ; » et « » » qu'exige le français
- « programmation militaire 2024-2030 » lu comme la loi n° 2024-2030

make seed : 52 textes, 368 sources, 83 événements, 12 affaires CC, 7 analyses.
Zéro texte sans source URL (critère §8.2). Les 2 textes sans résumé et les 17
pertinences déduites sont marqués « à vérifier » avec leur motif.

99 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:43:03 -04:00

227 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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