Files
veye-lalwa/pipeline/parseurs/notes_bas_page.py
T
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

129 lines
4.4 KiB
Python
Raw 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.
"""Lecture des listes de notes en fin de rapport de dimension.
Cinq dimensions (dim06, dim08, dim09, dim11, dim12) ne portent pas — ou peu —
de blocs sourcés : leurs références vivent dans une liste finale du type ::
[^1141^] senat.fr — Texte n° 890, projet de loi cohésion républicaine
(09/07/2026) — https://www.senat.fr/leg/pjl25-890.html
[^44^] diplomatie.gouv.fr, 16/04/2026.
Toutes ne portent pas d'URL : sur les 258 notes du corpus, 150 en sont
dépourvues. Deux cas sont alors distingués :
- la note cite un identifiant Légifrance (``JORFTEXT…``, ``JORFARTI…``) : l'URL
canonique est reconstruite, ce qui n'invente rien puisque le schéma d'URL est
fixe et documenté ;
- sinon la référence est conservée telle quelle, sans URL, et comptée comme
« non résolue » dans le compte-rendu d'import. Elle pourra être rapprochée de
la bibliographie consolidée, qui, elle, est intégralement liée.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from datetime import date
from pipeline.modeles import Confiance, Source
from pipeline.parseurs.dates_fr import lire_date
from pipeline.parseurs.markdown import nettoyer
ANNEE_CORPUS = 2026
_LIGNE_NOTE = re.compile(r"^\[\^([0-9.]+)\^\]\s+(.*)$")
_URL = re.compile(r"https?://[^\s;,)\]]+")
_IDENTIFIANT_LEGIFRANCE = re.compile(r"\b(JORFTEXT\d{12}|JORFARTI\d{12}|LEGITEXT\d{12})\b")
_DATE_PARENTHESES = re.compile(r"\(([^)]*\d{4}[^)]*)\)")
# Schéma d'URL documenté de Légifrance : un identifiant JORF s'ouvre toujours à
# cette adresse. Reconstruire le lien n'ajoute aucune information nouvelle.
_GABARIT_LEGIFRANCE = "https://www.legifrance.gouv.fr/jorf/id/{identifiant}"
@dataclass(slots=True)
class Note:
"""Une référence numérotée en fin de rapport de dimension."""
dimension: str
numero: str
texte: str
url: str | None = None
editeur: str | None = None
date_publication: date | None = None
url_reconstruite: bool = False
@property
def marqueur(self) -> str:
return f"{self.dimension}-{self.numero}"
def en_source(self) -> Source | None:
"""Convertit la note en source, ou `None` si elle n'a pas d'URL."""
if not self.url:
return None
return Source(
url=self.url,
titre=self.texte[:280] or None,
editeur=self.editeur,
date_publication=self.date_publication,
confiance=Confiance.MEDIUM,
marqueur=self.marqueur,
fichier_origine=f"research/lois-2026_{self.dimension}.md",
)
def parser(markdown: str, dimension: str) -> list[Note]:
"""Extrait toutes les notes numérotées d'un rapport de dimension."""
notes: list[Note] = []
for ligne in markdown.splitlines():
correspondance = _LIGNE_NOTE.match(ligne.strip())
if not correspondance:
continue
notes.append(_lire_note(correspondance.group(1), correspondance.group(2), dimension))
return notes
def _lire_note(numero: str, corps: str, dimension: str) -> Note:
url: str | None = None
reconstruite = False
if trouvee := _URL.search(corps):
url = trouvee.group(0).rstrip(".,;)")
elif identifiant := _IDENTIFIANT_LEGIFRANCE.search(corps):
url = _GABARIT_LEGIFRANCE.format(identifiant=identifiant.group(1))
reconstruite = True
return Note(
dimension=dimension,
numero=numero,
texte=nettoyer(_sans_url(corps)),
url=url,
editeur=_lire_editeur(corps),
date_publication=_lire_date_note(corps),
url_reconstruite=reconstruite,
)
def _sans_url(corps: str) -> str:
return _URL.sub("", corps).strip(" —–-:.,")
def _lire_editeur(corps: str) -> str | None:
"""L'éditeur ouvre la note, avant le tiret cadratin ou la première virgule."""
debut = _sans_url(corps)
for separateur in ("—", "", " : ", ","):
if separateur in debut:
debut = debut.split(separateur, 1)[0]
break
debut = nettoyer(debut).strip(" .")
return debut[:120] or None
def _lire_date_note(corps: str) -> date | None:
"""Date entre parenthèses en priorité, sinon première date du corps."""
for parenthese in _DATE_PARENTHESES.findall(corps):
if lue := lire_date(parenthese, annee_defaut=ANNEE_CORPUS):
return lue
return lire_date(_sans_url(corps), annee_defaut=ANNEE_CORPUS)