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