2026-07-25 19:43:03 -04:00
|
|
|
"""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
|
|
|
|
|
|
2026-07-25 20:23:51 -04:00
|
|
|
from pipeline import chemins, db, guadeloupe, impacts, resolution_conflits
|
2026-07-25 19:43:03 -04:00
|
|
|
from pipeline.journal import configurer, logger
|
|
|
|
|
from pipeline.modeles import Source, Texte
|
|
|
|
|
from pipeline.parseurs import (
|
|
|
|
|
analyses,
|
|
|
|
|
bibliographie,
|
|
|
|
|
chapitres,
|
|
|
|
|
citations,
|
2026-07-25 20:23:51 -04:00
|
|
|
echeances,
|
2026-07-25 19:43:03 -04:00
|
|
|
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
|
2026-07-25 20:23:51 -04:00
|
|
|
echeances: int = 0
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
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)
|
2026-07-25 20:23:51 -04:00
|
|
|
impacts_deduits: list[str] = field(default_factory=list)
|
|
|
|
|
verdicts_appliques: int = 0
|
2026-07-25 19:43:03 -04:00
|
|
|
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}",
|
2026-07-25 20:23:51 -04:00
|
|
|
f" échéances : {self.echeances}",
|
2026-07-25 19:43:03 -04:00
|
|
|
"",
|
|
|
|
|
"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)}",
|
2026-07-25 20:23:51 -04:00
|
|
|
f" impacts déduits : {len(self.impacts_deduits)}",
|
|
|
|
|
f" arbitrages appliqués : {self.verdicts_appliques}",
|
2026-07-25 19:43:03 -04:00
|
|
|
]
|
|
|
|
|
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)
|
2026-07-25 20:23:51 -04:00
|
|
|
_ventiler_impacts(texte, lecture, compte)
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-07-25 20:23:51 -04:00
|
|
|
# 5. Arbitrages de la validation de phase 5 — le §7 du cahier des charges
|
|
|
|
|
# exige que la loi « Riposte » porte l'AFD à 500 € et non à 1 500 €.
|
|
|
|
|
verdicts = resolution_conflits.lire_verdicts(
|
|
|
|
|
chemins.PHASE5_VALIDATION.read_text(encoding="utf-8")
|
|
|
|
|
)
|
|
|
|
|
compte.verdicts_appliques = resolution_conflits.appliquer(textes, verdicts)
|
|
|
|
|
|
|
|
|
|
# 6. Écriture.
|
2026-07-25 19:43:03 -04:00
|
|
|
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)
|
2026-07-25 20:23:51 -04:00
|
|
|
for echeance in echeances.echeances_nationales():
|
|
|
|
|
db.enregistrer_echeance(cx, echeance)
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
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"]
|
2026-07-25 20:23:51 -04:00
|
|
|
compte.echeances = statistiques["echeances"]
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 20:23:51 -04:00
|
|
|
def _ventiler_impacts(
|
|
|
|
|
texte: Texte, lecture: chapitres.LectureChapitres, compte: CompteRendu
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Répartit l'effet du texte entre les trois publics suivis."""
|
|
|
|
|
ventilation = impacts.ventiler(texte.id, lecture, regime=texte.regime_libertes)
|
|
|
|
|
if not ventilation.impacts:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
texte.impacts = ventilation.impacts
|
|
|
|
|
if ventilation.deduit:
|
|
|
|
|
texte.a_verifier = True
|
|
|
|
|
texte.motif_verification = _ajouter_motif(
|
|
|
|
|
texte.motif_verification, impacts.MOTIF_VERIFICATION
|
|
|
|
|
)
|
|
|
|
|
compte.impacts_deduits.append(texte.id)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 19:43:03 -04:00
|
|
|
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()
|