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:
co-authored by
Claude Opus 5
parent
34d81c66a8
commit
29424afdd5
@@ -16,16 +16,23 @@ entreprises**, avec un zoom Guadeloupe et outre-mer.
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Textes législatifs | **52** — 27 promulgués, 7 devant le Conseil constitutionnel, 10 en navette, 5 déposés, 2 annoncés, 1 validé |
|
| Textes législatifs | **59** — 52 issus du corpus de recherche, 7 ajoutés par le pipeline depuis les sources officielles |
|
||||||
| Sources vérifiables | **360**, toutes avec adresse — aucun texte sans lien |
|
| Sources vérifiables | **367**, toutes avec adresse — aucun texte sans lien |
|
||||||
| Événements de parcours | 83 |
|
| Événements de parcours | 90 |
|
||||||
| Affaires du Conseil constitutionnel | 12 |
|
| Affaires du Conseil constitutionnel | 13 |
|
||||||
| Analyses transversales | 7 |
|
| Analyses transversales | 7 |
|
||||||
| Échéances de calendrier | 9 |
|
| Échéances de calendrier | 9 |
|
||||||
| Marqueurs de citation résolus | **100 %** (332/332), dont 93 % avec un lien |
|
| Marqueurs de citation résolus | **100 %** (332/332), dont 93 % avec un lien |
|
||||||
|
|
||||||
Les valeurs déduites — 16 cotations Guadeloupe, 17 ventilations d'impact — sont
|
Les valeurs déduites sont signalées comme telles, avec leur motif : 16 cotations
|
||||||
signalées comme telles, avec leur motif.
|
Guadeloupe, 17 ventilations d'impact, et les 7 textes ajoutés par le pipeline —
|
||||||
|
qui entrent tous avec la confiance la plus basse et le drapeau de revue.
|
||||||
|
|
||||||
|
Les 52 textes du corpus couvrent la fenêtre du 1ᵉʳ mai au 30 septembre 2026. Les
|
||||||
|
7 ajouts sont antérieurs (janvier à avril 2026) : le pipeline les a relevés sur
|
||||||
|
la liste du Sénat, qui couvre toute la session. Parmi eux, la loi de finances
|
||||||
|
pour 2026 (n° 2026-103), qui modifie l'octroi de mer et étend le versement
|
||||||
|
mobilité aux départements d'outre-mer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+58
-6
@@ -51,6 +51,13 @@ class Ecart:
|
|||||||
nouvelle_valeur: str | None = None
|
nouvelle_valeur: str | None = None
|
||||||
description: str = ""
|
description: str = ""
|
||||||
collecteur: 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:
|
def en_ligne(self) -> str:
|
||||||
cible = self.texte_id or "(nouveau)"
|
cible = self.texte_id or "(nouveau)"
|
||||||
@@ -164,7 +171,9 @@ def comparer(cx: sqlite3.Connection, collectes: list[ResultatCollecte]) -> Rappo
|
|||||||
for texte in collecte.textes:
|
for texte in collecte.textes:
|
||||||
_comparer_texte(texte, candidats, connus, rapport, collecte.collecteur)
|
_comparer_texte(texte, candidats, connus, rapport, collecte.collecteur)
|
||||||
for decision in collecte.decisions_cc:
|
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
|
return rapport
|
||||||
|
|
||||||
@@ -187,6 +196,7 @@ def _comparer_texte(
|
|||||||
nature="ajout",
|
nature="ajout",
|
||||||
description=f"{collecte.numero_officiel or '—'} · {collecte.titre[:110]}",
|
description=f"{collecte.numero_officiel or '—'} · {collecte.titre[:110]}",
|
||||||
collecteur=collecteur,
|
collecteur=collecteur,
|
||||||
|
collecte=collecte,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -245,19 +255,33 @@ def _comparer_decision(
|
|||||||
connues: dict[str, sqlite3.Row],
|
connues: dict[str, sqlite3.Row],
|
||||||
rapport: RapportRun,
|
rapport: RapportRun,
|
||||||
collecteur: str,
|
collecteur: str,
|
||||||
|
candidats: list[tuple[str, str, str | None]] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
existante = connues.get(decision.numero_affaire)
|
existante = connues.get(decision.numero_affaire)
|
||||||
|
|
||||||
if existante is None:
|
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(
|
rapport.ecarts.append(
|
||||||
Ecart(
|
Ecart(
|
||||||
texte_id=None,
|
texte_id=rattachement,
|
||||||
nature="decision_cc",
|
nature="decision_cc",
|
||||||
description=(
|
description=(
|
||||||
f"affaire inconnue en base : {decision.numero_affaire} — "
|
f"affaire inconnue en base : {decision.numero_affaire} — "
|
||||||
f"{(decision.resume or '')[:90]}"
|
f"{(decision.resume or '')[:90]}"
|
||||||
|
+ (f" → rattachée à {rattachement}" if rattachement else "")
|
||||||
),
|
),
|
||||||
collecteur=collecteur,
|
collecteur=collecteur,
|
||||||
|
decision=decision,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -271,6 +295,7 @@ def _comparer_decision(
|
|||||||
ancienne_valeur=None,
|
ancienne_valeur=None,
|
||||||
nouvelle_valeur=decision.date_decision.isoformat(),
|
nouvelle_valeur=decision.date_decision.isoformat(),
|
||||||
collecteur=collecteur,
|
collecteur=collecteur,
|
||||||
|
decision=decision,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,6 +308,7 @@ def _comparer_decision(
|
|||||||
ancienne_valeur=None,
|
ancienne_valeur=None,
|
||||||
nouvelle_valeur=decision.date_saisine.isoformat(),
|
nouvelle_valeur=decision.date_saisine.isoformat(),
|
||||||
collecteur=collecteur,
|
collecteur=collecteur,
|
||||||
|
decision=decision,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -299,6 +325,7 @@ def _comparer_decision(
|
|||||||
ancienne_valeur=existante["resultat"],
|
ancienne_valeur=existante["resultat"],
|
||||||
nouvelle_valeur=str(decision.resultat),
|
nouvelle_valeur=str(decision.resultat),
|
||||||
collecteur=collecteur,
|
collecteur=collecteur,
|
||||||
|
decision=decision,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -306,9 +333,23 @@ def _comparer_decision(
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Application
|
# Application
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> None:
|
def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> int:
|
||||||
"""Écrit les écarts en base. Jamais appelé en mode `--dry-run`."""
|
"""É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):
|
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:
|
for ecart in rapport.ecarts:
|
||||||
db.enregistrer_changement(
|
db.enregistrer_changement(
|
||||||
cx,
|
cx,
|
||||||
@@ -321,6 +362,10 @@ def appliquer(cx: sqlite3.Connection, rapport: RapportRun, run_id: int) -> None:
|
|||||||
description=ecart.description or 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"):
|
if ecart.texte_id and ecart.champ and ecart.nature in ("statut", "date"):
|
||||||
cx.execute(
|
cx.execute(
|
||||||
f"UPDATE textes SET {ecart.champ} = ?, maj_le = datetime('now'), " # noqa: S608
|
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"
|
"WHERE numero_officiel IS NOT NULL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return crees
|
||||||
|
|
||||||
|
|
||||||
_NATURES_SQL = {"ajout", "statut", "date", "source", "autre"}
|
_NATURES_SQL = {"ajout", "statut", "date", "source", "autre"}
|
||||||
|
|
||||||
@@ -470,9 +517,14 @@ def main() -> None:
|
|||||||
|
|
||||||
run_id = db.ouvrir_run(cx, mode)
|
run_id = db.ouvrir_run(cx, mode)
|
||||||
if not arguments.dry_run:
|
if not arguments.dry_run:
|
||||||
appliquer(cx, rapport, run_id)
|
crees = appliquer(cx, rapport, run_id)
|
||||||
exportes = exporter_json(cx)
|
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(
|
db.cloturer_run(
|
||||||
cx,
|
cx,
|
||||||
|
|||||||
@@ -364,3 +364,117 @@ def test_la_negation_ne_porte_que_sur_son_voisinage() -> None:
|
|||||||
+ "Le dispositif est sans objet pour les collectivités du Pacifique."
|
+ "Le dispositif est sans objet pour les collectivités du Pacifique."
|
||||||
)
|
)
|
||||||
assert cotation.pertinence is Pertinence.FORTE
|
assert cotation.pertinence is Pertinence.FORTE
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Application effective : créer, mettre à jour, converger
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_ajout_cree_reellement_le_texte(base) -> None:
|
||||||
|
"""Un écart d'ajout doit écrire le texte, pas seulement le journaliser."""
|
||||||
|
collecte = _collecte(
|
||||||
|
textes=[
|
||||||
|
TexteCollecte(
|
||||||
|
titre="LOI n° 2026-103 du 19 février 2026 de finances pour 2026",
|
||||||
|
source_url="https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000999",
|
||||||
|
collecteur="senat",
|
||||||
|
numero_officiel="2026-103",
|
||||||
|
statut=Statut.PROMULGUEE,
|
||||||
|
date_promulgation=date(2026, 2, 19),
|
||||||
|
sources=[
|
||||||
|
Source(
|
||||||
|
url="https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000999",
|
||||||
|
editeur="Légifrance",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
rapport = comparer(base, [collecte])
|
||||||
|
crees = appliquer(base, rapport, db.ouvrir_run(base, "update"))
|
||||||
|
|
||||||
|
assert crees == 1
|
||||||
|
ligne = base.execute("SELECT * FROM textes WHERE id = 'loi-2026-103'").fetchone()
|
||||||
|
assert ligne is not None
|
||||||
|
assert ligne["confiance"] == "low"
|
||||||
|
assert ligne["a_verifier"] == 1
|
||||||
|
assert ligne["source_seed"] == "collecteur:senat"
|
||||||
|
# Un texte sans source serait un texte invérifiable : la collecte doit
|
||||||
|
# transmettre la sienne.
|
||||||
|
assert base.execute(
|
||||||
|
"SELECT COUNT(*) FROM sources WHERE texte_id = 'loi-2026-103'"
|
||||||
|
).fetchone()[0] >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_une_decision_collectee_est_ecrite_pas_seulement_journalisee(base) -> None:
|
||||||
|
"""Sans écriture, le même écart serait redétecté à chaque passage."""
|
||||||
|
collecte = _collecte(
|
||||||
|
decisions_cc=[
|
||||||
|
DecisionCC(
|
||||||
|
numero_affaire="2026-915 DC",
|
||||||
|
date_decision=date(2026, 8, 24),
|
||||||
|
resultat=ResultatCC.NON_CONFORMITE_PARTIELLE,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
rapport = comparer(base, [collecte])
|
||||||
|
appliquer(base, rapport, db.ouvrir_run(base, "update"))
|
||||||
|
|
||||||
|
ligne = base.execute(
|
||||||
|
"SELECT * FROM decisions_cc WHERE numero_affaire = '2026-915 DC'"
|
||||||
|
).fetchone()
|
||||||
|
assert ligne["date_decision"] == "2026-08-24"
|
||||||
|
assert ligne["resultat"] == "non_conformite_partielle"
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_pipeline_converge(base) -> None:
|
||||||
|
"""Rejouer la même collecte après application ne doit plus rien signaler.
|
||||||
|
|
||||||
|
C'est la propriété qui rend une veille utilisable : si le rapport répétait
|
||||||
|
les mêmes écarts à chaque passage, il deviendrait illisible et on cesserait
|
||||||
|
de le lire.
|
||||||
|
"""
|
||||||
|
collecte = _collecte(
|
||||||
|
textes=[
|
||||||
|
TexteCollecte(
|
||||||
|
titre="LOI n° 2026-103 du 19 février 2026 de finances pour 2026",
|
||||||
|
source_url="https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000999",
|
||||||
|
collecteur="senat",
|
||||||
|
numero_officiel="2026-103",
|
||||||
|
statut=Statut.PROMULGUEE,
|
||||||
|
date_promulgation=date(2026, 2, 19),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
decisions_cc=[
|
||||||
|
DecisionCC(
|
||||||
|
numero_affaire="2026-915 DC",
|
||||||
|
date_decision=date(2026, 8, 24),
|
||||||
|
resultat=ResultatCC.NON_CONFORMITE_PARTIELLE,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
premier = comparer(base, [collecte])
|
||||||
|
assert premier.ecarts, "le premier passage doit voir des écarts"
|
||||||
|
appliquer(base, premier, db.ouvrir_run(base, "update"))
|
||||||
|
|
||||||
|
second = comparer(base, [collecte])
|
||||||
|
assert second.ecarts == [], "le second passage ne doit plus rien signaler"
|
||||||
|
|
||||||
|
|
||||||
|
def test_une_affaire_orpheline_est_rattachee_par_son_intitule(base) -> None:
|
||||||
|
"""Cas réel : le Conseil donne l'intitulé complet, la base le nom d'usage."""
|
||||||
|
collecte = _collecte(
|
||||||
|
decisions_cc=[
|
||||||
|
DecisionCC(
|
||||||
|
numero_affaire="2026-916 DC",
|
||||||
|
date_saisine=date(2026, 8, 1),
|
||||||
|
resume="Loi « Riposte » ordre public sécurité tranquillité",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
rapport = comparer(base, [collecte])
|
||||||
|
(ecart,) = [e for e in rapport.ecarts if e.nature == "decision_cc"]
|
||||||
|
assert ecart.texte_id == "loi-riposte"
|
||||||
|
assert "rattachée à loi-riposte" in ecart.description
|
||||||
|
|||||||
Reference in New Issue
Block a user