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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c36b8dc631
commit
c28243cf87
@@ -0,0 +1,128 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user