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,294 @@
|
||||
"""Remplissage initial de la base à partir du corpus de recherche.
|
||||
|
||||
Enchaînement :
|
||||
|
||||
1. **Annexe** (`sec10.md`) — 52 textes avec statut, dates, cotation des
|
||||
libertés et pertinence Guadeloupe pour les lois promulguées.
|
||||
2. **Citations** — index de résolution des marqueurs `[^dimXX-N^]` vers les
|
||||
sources vérifiables des douze rapports de dimension.
|
||||
3. **Chapitres** (`sec00` → `sec09`) — résumés, points clés et marqueurs
|
||||
supplémentaires, rattachés phrase par phrase.
|
||||
4. **Bibliographie** — filet de sécurité : toute loi promulguée reçoit les
|
||||
références consolidées qui citent son numéro.
|
||||
5. **Analyses** (`insight.md`) — sept lectures transversales.
|
||||
|
||||
Rien n'est écrit qui ne vienne d'un fichier de `data/input/`, lequel n'est
|
||||
jamais modifié. Le compte-rendu d'import indique fichier par fichier ce qui a
|
||||
été lu, ce qui a été rattaché et ce qui manque.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from pipeline import chemins, db, guadeloupe
|
||||
from pipeline.journal import configurer, logger
|
||||
from pipeline.modeles import Source, Texte
|
||||
from pipeline.parseurs import (
|
||||
analyses,
|
||||
bibliographie,
|
||||
chapitres,
|
||||
citations,
|
||||
tableaux_sec10,
|
||||
)
|
||||
|
||||
log = logger("seed")
|
||||
|
||||
# Plancher fixé au §2 du cahier des charges.
|
||||
TEXTES_ATTENDUS_MINIMUM = 49
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompteRendu:
|
||||
"""Ce que le seed a lu, écrit et manqué."""
|
||||
|
||||
textes: int = 0
|
||||
sources: int = 0
|
||||
evenements: int = 0
|
||||
decisions_cc: int = 0
|
||||
analyses: int = 0
|
||||
|
||||
textes_sans_source: list[str] = field(default_factory=list)
|
||||
textes_sans_resume: list[str] = field(default_factory=list)
|
||||
pertinences_deduites: list[str] = field(default_factory=list)
|
||||
avertissements: list[str] = field(default_factory=list)
|
||||
|
||||
marqueurs_avec_lien: int = 0
|
||||
marqueurs_sans_lien: int = 0
|
||||
marqueurs_non_resolus: int = 0
|
||||
|
||||
def en_texte(self) -> str:
|
||||
lignes = [
|
||||
"Compte-rendu d'import du corpus de recherche",
|
||||
"=" * 44,
|
||||
f" textes : {self.textes}",
|
||||
f" sources : {self.sources}",
|
||||
f" événements : {self.evenements}",
|
||||
f" décisions CC : {self.decisions_cc}",
|
||||
f" analyses : {self.analyses}",
|
||||
"",
|
||||
"Résolution des citations",
|
||||
f" avec lien : {self.marqueurs_avec_lien}",
|
||||
f" sans lien (réf.) : {self.marqueurs_sans_lien}",
|
||||
f" non résolus : {self.marqueurs_non_resolus}",
|
||||
"",
|
||||
"Points d'attention",
|
||||
f" textes sans source URL : {len(self.textes_sans_source)}"
|
||||
+ (f" → {', '.join(self.textes_sans_source)}" if self.textes_sans_source else ""),
|
||||
f" textes sans résumé : {len(self.textes_sans_resume)}"
|
||||
+ (f" → {', '.join(self.textes_sans_resume)}" if self.textes_sans_resume else ""),
|
||||
f" pertinences déduites : {len(self.pertinences_deduites)}",
|
||||
]
|
||||
if self.avertissements:
|
||||
lignes.append("")
|
||||
lignes.append("Avertissements")
|
||||
lignes.extend(f" ! {a}" for a in self.avertissements)
|
||||
return "\n".join(lignes)
|
||||
|
||||
|
||||
def ensemencer(cx: sqlite3.Connection) -> CompteRendu:
|
||||
"""Construit le jeu de données initial et l'écrit en base."""
|
||||
compte = CompteRendu()
|
||||
|
||||
manquants = chemins.verifier_corpus()
|
||||
if manquants:
|
||||
compte.avertissements.extend(f"fichier d'entrée absent : {m}" for m in manquants)
|
||||
|
||||
# 1. Annexe récapitulative — le socle factuel.
|
||||
annexe = tableaux_sec10.parser(chemins.chapitre(10).read_text(encoding="utf-8"))
|
||||
compte.avertissements.extend(annexe.avertissements)
|
||||
textes: dict[str, Texte] = {texte.id: texte for texte in annexe.textes}
|
||||
marqueurs_annexe = annexe.marqueurs
|
||||
|
||||
# 2. Index de résolution des citations.
|
||||
repertoire = citations.construire()
|
||||
|
||||
# 3. Chapitres rédigés.
|
||||
lecture = chapitres.parser()
|
||||
|
||||
# 4. Enrichissement texte par texte.
|
||||
for texte in textes.values():
|
||||
_rattacher_sources(texte, repertoire, lecture, marqueurs_annexe)
|
||||
_rattacher_redaction(texte, lecture)
|
||||
_coter_guadeloupe(texte, compte)
|
||||
|
||||
if not texte.sources:
|
||||
compte.textes_sans_source.append(texte.id)
|
||||
if not texte.resume:
|
||||
compte.textes_sans_resume.append(texte.id)
|
||||
texte.a_verifier = True
|
||||
texte.motif_verification = _ajouter_motif(
|
||||
texte.motif_verification,
|
||||
"Aucun paragraphe du rapport ne développe ce texte : "
|
||||
"seul l'essentiel de l'annexe est disponible.",
|
||||
)
|
||||
|
||||
compte.marqueurs_avec_lien = len(repertoire.resolus_avec_lien)
|
||||
compte.marqueurs_sans_lien = len(repertoire.resolus_sans_lien)
|
||||
compte.marqueurs_non_resolus = len(repertoire.non_resolus)
|
||||
|
||||
# 5. Écriture.
|
||||
db.enregistrer_textes(cx, textes.values())
|
||||
|
||||
lectures = analyses.parser(chemins.INSIGHTS.read_text(encoding="utf-8"))
|
||||
with db.transaction(cx):
|
||||
for analyse in lectures:
|
||||
db.enregistrer_insight(cx, analyse)
|
||||
|
||||
statistiques = db.statistiques(cx)
|
||||
compte.textes = statistiques["textes"]
|
||||
compte.sources = statistiques["sources"]
|
||||
compte.evenements = statistiques["evenements"]
|
||||
compte.decisions_cc = statistiques["decisions_cc"]
|
||||
compte.analyses = statistiques["insights"]
|
||||
|
||||
if compte.textes < TEXTES_ATTENDUS_MINIMUM:
|
||||
compte.avertissements.append(
|
||||
f"{compte.textes} textes importés, {TEXTES_ATTENDUS_MINIMUM} attendus au minimum"
|
||||
)
|
||||
|
||||
return compte
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Enrichissement
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def _rattacher_sources(
|
||||
texte: Texte,
|
||||
repertoire: citations.Repertoire,
|
||||
lecture: chapitres.LectureChapitres,
|
||||
marqueurs_annexe: dict[str, list[str]],
|
||||
) -> None:
|
||||
"""Attache à un texte toutes les sources que le corpus lui rattache."""
|
||||
# Marqueurs portés par sa ligne d'annexe, puis par les chapitres.
|
||||
marqueurs = list(marqueurs_annexe.get(texte.id, []))
|
||||
marqueurs.extend(lecture.marqueurs_de(texte.id))
|
||||
|
||||
sources = repertoire.resoudre_plusieurs(marqueurs)
|
||||
|
||||
# Filet : la bibliographie consolidée. Trois clés d'accroche, du plus
|
||||
# spécifique au moins spécifique — un texte non promulgué n'a pas de numéro
|
||||
# de loi, sa seule identité est sa cote parlementaire (« AN n° 3025 »).
|
||||
affaires = [decision.numero_affaire for decision in texte.decisions_cc]
|
||||
documents = bibliographie.numeros_de_document(
|
||||
f"{texte.titre_court} {texte.titre_officiel or ''}"
|
||||
)
|
||||
sources.extend(
|
||||
repertoire.references_du_texte(
|
||||
numero_loi=texte.numero_officiel, affaires=affaires, documents=documents
|
||||
)
|
||||
)
|
||||
|
||||
vues: set[str] = set()
|
||||
retenues: list[Source] = []
|
||||
for source in sources:
|
||||
if source.url in vues:
|
||||
continue
|
||||
vues.add(source.url)
|
||||
retenues.append(source)
|
||||
|
||||
texte.sources = retenues
|
||||
|
||||
|
||||
def _rattacher_redaction(texte: Texte, lecture: chapitres.LectureChapitres) -> None:
|
||||
"""Reprend résumé et points clés du rapport, sans les reformuler."""
|
||||
if resume := lecture.resume_de(texte.id):
|
||||
texte.resume = resume
|
||||
|
||||
# Les points clés de l'annexe (colonne « Enjeu libertés ») sont déjà posés
|
||||
# par le parseur de tableaux ; on complète avec la note de régime.
|
||||
if texte.regime_libertes_note and texte.regime_libertes_note not in texte.points_cles:
|
||||
texte.points_cles.append(texte.regime_libertes_note)
|
||||
if texte.guadeloupe_note and texte.guadeloupe_note not in texte.points_cles:
|
||||
texte.points_cles.append(texte.guadeloupe_note)
|
||||
|
||||
|
||||
def _coter_guadeloupe(texte: Texte, compte: CompteRendu) -> None:
|
||||
"""Cote la pertinence guadeloupéenne des textes que l'annexe ne cote pas.
|
||||
|
||||
Les 27 lois promulguées portent la cotation manuelle du rapport : elle est
|
||||
conservée telle quelle, conformément au §5.3 du cahier des charges.
|
||||
"""
|
||||
if texte.guadeloupe_pertinence is not None:
|
||||
return
|
||||
|
||||
cotation = guadeloupe.coter(
|
||||
texte.titre_court,
|
||||
texte.titre_officiel,
|
||||
texte.resume,
|
||||
" ".join(texte.points_cles),
|
||||
texte.guadeloupe_note,
|
||||
)
|
||||
if cotation.pertinence is None:
|
||||
return
|
||||
|
||||
texte.guadeloupe_pertinence = cotation.pertinence
|
||||
texte.guadeloupe_note = texte.guadeloupe_note or cotation.note
|
||||
texte.a_verifier = True
|
||||
texte.motif_verification = _ajouter_motif(
|
||||
texte.motif_verification, guadeloupe.MOTIF_VERIFICATION
|
||||
)
|
||||
compte.pertinences_deduites.append(texte.id)
|
||||
|
||||
|
||||
def _ajouter_motif(existant: str | None, nouveau: str) -> str:
|
||||
if not existant:
|
||||
return nouveau
|
||||
if nouveau in existant:
|
||||
return existant
|
||||
return f"{existant} {nouveau}"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Ligne de commande
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def main() -> None:
|
||||
analyseur = argparse.ArgumentParser(
|
||||
description="Remplit la base à partir du corpus de recherche de data/input/."
|
||||
)
|
||||
analyseur.add_argument(
|
||||
"--repartir-de-zero",
|
||||
action="store_true",
|
||||
help="supprime la base existante avant l'import",
|
||||
)
|
||||
analyseur.add_argument("--verbeux", action="store_true", help="journalisation détaillée")
|
||||
arguments = analyseur.parse_args()
|
||||
|
||||
configurer("DEBUG" if arguments.verbeux else "INFO")
|
||||
depart = time.monotonic()
|
||||
|
||||
cx = db.initialiser(repartir_de_zero=arguments.repartir_de_zero)
|
||||
run_id = db.ouvrir_run(cx, "seed")
|
||||
compte = ensemencer(cx)
|
||||
duree = time.monotonic() - depart
|
||||
|
||||
db.cloturer_run(
|
||||
cx,
|
||||
run_id,
|
||||
duree_s=duree,
|
||||
ajouts=compte.textes,
|
||||
echecs=len(compte.avertissements),
|
||||
alertes=compte.avertissements,
|
||||
rapport=compte.en_texte(),
|
||||
)
|
||||
|
||||
print()
|
||||
print(compte.en_texte())
|
||||
print()
|
||||
log.info(
|
||||
"seed terminé",
|
||||
duree_s=round(duree, 2),
|
||||
textes=compte.textes,
|
||||
sources=compte.sources,
|
||||
base=str(chemins.BASE_SQLITE),
|
||||
)
|
||||
|
||||
if compte.textes < TEXTES_ATTENDUS_MINIMUM:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user