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