Intégration des 7 textes détectés par le pipeline — la veille converge

make update écrivait jusqu'ici les changements de statut et de date, mais se
contentait de journaliser les ajouts et les décisions du Conseil : les mêmes
écarts seraient revenus à chaque passage, indéfiniment. Un rapport qui répète
les mêmes lignes cesse d'être lu.

Deux manques comblés :
- un écart d'ajout porte désormais la collecte qui l'a produit, et appliquer()
  crée réellement le texte via classer_nouveau()
- un écart de décision porte l'affaire collectée, et l'écrit dans decisions_cc

Une affaire orpheline est rattachée par similarité de titre : le registre du
Conseil donne l'intitulé complet (« Loi actualisant la programmation militaire
pour les années 2024 à 2030… »), la base le nom d'usage. L'affaire 2026-907 DC
a ainsi trouvé sa loi toute seule.

Résultat sur les sources réelles :
  passage 1 → 7 ajouts, 4 modifications
  passage 2 → 0 ajout,  4 modifications (décisions désormais écrites)
  passage 3 → 0 écart : « la base est conforme aux sources officielles »

Les 7 textes ajoutés sont des lois de janvier à avril 2026, hors de la fenêtre
du corpus mais relevées par la liste du Sénat qui couvre toute la session. Dont
la loi de finances pour 2026 (n° 2026-103), qui touche l'octroi de mer et étend
le versement mobilité aux DROM — donc pertinente pour la Guadeloupe. Toutes
entrent avec confiance = low et le drapeau de revue : le pipeline constate
qu'un texte existe, il ne prétend pas l'avoir analysé.

Base : 59 textes, 367 sources, 13 affaires CC, 0 texte sans lien.
176 tests, make verifier au vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cyber Mawonaj
2026-07-25 22:41:18 -04:00
co-authored by Claude Opus 5
parent 34d81c66a8
commit 29424afdd5
3 changed files with 185 additions and 12 deletions
+58 -6
View File
@@ -51,6 +51,13 @@ class Ecart:
nouvelle_valeur: str | None = None
description: str = ""
collecteur: str = ""
# Renseignée pour les ajouts : c'est elle qui permet de créer le texte au
# moment d'appliquer, sans relancer une collecte.
collecte: TexteCollecte | None = None
# Idem pour les affaires du Conseil constitutionnel. Sans elle, un écart de
# décision serait journalisé mais jamais écrit — et redétecté à chaque
# passage, indéfiniment.
decision: DecisionCC | None = None
def en_ligne(self) -> str:
cible = self.texte_id or "(nouveau)"
@@ -164,7 +171,9 @@ def comparer(cx: sqlite3.Connection, collectes: list[ResultatCollecte]) -> Rappo
for texte in collecte.textes:
_comparer_texte(texte, candidats, connus, rapport, collecte.collecteur)
for decision in collecte.decisions_cc:
_comparer_decision(decision, affaires_connues, rapport, collecte.collecteur)
_comparer_decision(
decision, affaires_connues, rapport, collecte.collecteur, candidats
)
return rapport
@@ -187,6 +196,7 @@ def _comparer_texte(
nature="ajout",
description=f"{collecte.numero_officiel or ''} · {collecte.titre[:110]}",
collecteur=collecteur,
collecte=collecte,
)
)
return
@@ -245,19 +255,33 @@ def _comparer_decision(
connues: dict[str, sqlite3.Row],
rapport: RapportRun,
collecteur: str,
candidats: list[tuple[str, str, str | None]] | None = None,
) -> None:
existante = connues.get(decision.numero_affaire)
if existante is None:
# Le registre du Conseil donne l'intitulé complet de la loi déférée :
# on tente de la rattacher à un texte suivi plutôt que de laisser
# l'affaire orpheline.
rattachement = None
if candidats and decision.resume:
correspondance = rapprocher(
titre=decision.resume, numero_officiel=None, candidats=candidats
)
if correspondance.issue is Issue.RAPPROCHE:
rattachement = correspondance.texte_id
rapport.ecarts.append(
Ecart(
texte_id=None,
texte_id=rattachement,
nature="decision_cc",
description=(
f"affaire inconnue en base : {decision.numero_affaire}"
f"{(decision.resume or '')[:90]}"
+ (f" → rattachée à {rattachement}" if rattachement else "")
),
collecteur=collecteur,
decision=decision,
)
)
return
@@ -271,6 +295,7 @@ def _comparer_decision(
ancienne_valeur=None,
nouvelle_valeur=decision.date_decision.isoformat(),
collecteur=collecteur,
decision=decision,
)
)
@@ -283,6 +308,7 @@ def _comparer_decision(
ancienne_valeur=None,
nouvelle_valeur=decision.date_saisine.isoformat(),
collecteur=collecteur,
decision=decision,
)
)
@@ -299,6 +325,7 @@ def _comparer_decision(
ancienne_valeur=existante["resultat"],
nouvelle_valeur=str(decision.resultat),
collecteur=collecteur,
decision=decision,
)
)
@@ -306,9 +333,23 @@ def _comparer_decision(
# ─────────────────────────────────────────────────────────────────────────────
# Application
# ─────────────────────────────────────────────────────────────────────────────
def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> None:
"""Écrit les écarts en base. Jamais appelé en mode `--dry-run`."""
def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> int:
"""Écrit les écarts en base. Jamais appelé en mode `--dry-run`.
Retourne le nombre de textes réellement créés. Les textes ajoutés par cette
voie entrent avec la confiance la plus basse et le drapeau de revue : la
machine constate qu'un texte existe, elle ne prétend pas l'avoir analysé.
"""
crees = 0
with db.transaction(cx):
for ecart in rapport.ecarts:
if ecart.nature == "ajout" and ecart.collecte is not None:
texte = classer_nouveau(ecart.collecte)
db.enregistrer_texte(cx, texte)
ecart.texte_id = texte.id
crees += 1
for ecart in rapport.ecarts:
db.enregistrer_changement(
cx,
@@ -321,6 +362,10 @@ def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> None:
description=ecart.description or None,
)
if ecart.nature == "decision_cc" and ecart.decision is not None:
db.enregistrer_decision_cc(cx, ecart.decision, texte_id=ecart.texte_id)
continue
if ecart.texte_id and ecart.champ and ecart.nature in ("statut", "date"):
cx.execute(
f"UPDATE textes SET {ecart.champ} = ?, maj_le = datetime('now'), " # noqa: S608
@@ -333,6 +378,8 @@ def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> None:
"WHERE numero_officiel IS NOT NULL"
)
return crees
_NATURES_SQL = {"ajout", "statut", "date", "source", "autre"}
@@ -470,9 +517,14 @@ def main() -> None:
run_id = db.ouvrir_run(cx, mode)
if not arguments.dry_run:
appliquer(cx, rapport, run_id)
crees = appliquer(cx, rapport, run_id)
exportes = exporter_json(cx)
log.info("dump statique régénéré", textes=exportes, fichier=str(chemins.DUMP_JSON))
log.info(
"écarts appliqués",
textes_crees=crees,
dump=str(chemins.DUMP_JSON),
textes_exportes=exportes,
)
db.cloturer_run(
cx,