Phase 4 : impacts par public, arbitrages, calendrier — les 7 critères §7 passent
Impacts : le rapport ne dresse pas de tableau texte × public, mais il consacre un chapitre à chacun (sec04 individus, sec05 associations, sec06 entreprises). C'est ce placement éditorial — un choix explicite de l'auteur, pas une inférence — qui sert de base à la ventilation. Le sens vient de la cotation manuelle de l'annexe quand elle existe, sinon du vocabulaire, et le texte est alors marqué « à vérifier » (17 cas). Arbitrages de la validation de phase 5 portés en points clés : loi-riposte affiche l'AFD à 500 €, et le montant de 1 500 € n'apparaît que présenté comme la version sénatoriale abandonnée. Migration 004 + calendrier : 9 échéances nationales transcrites du corpus avec leur extrait source. Celle des sénatoriales porte explicitement concerne_guadeloupe = 0 — série 1, renouvelée en 2023. Défaut de qualité corrigé : un titre de section nommant plusieurs textes (« Cohésion républicaine, logement, GloBE, entrisme ») faisait hériter chaque phrase de transition aux quatre, polluant leurs résumés. Une section n'est dédiée que si elle nomme un seul texte. 118 tests, 92 % de couverture. Les 7 garde-fous du §7 et les 5 démos du §8.7 sont automatisés dans tests/test_acceptation.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c28243cf87
commit
f3e546cfc4
@@ -223,6 +223,40 @@ def enregistrer_decision_cc(
|
||||
)
|
||||
|
||||
|
||||
def enregistrer_echeance(cx: sqlite3.Connection, echeance, *, texte_id: str | None = None) -> None:
|
||||
"""Écrit une échéance de calendrier (nationale ou rattachée à un texte)."""
|
||||
cx.execute(
|
||||
"""
|
||||
INSERT INTO echeances (code, date_echeance, libelle, description, portee,
|
||||
texte_id, concerne_guadeloupe, note_guadeloupe,
|
||||
previsionnel, source_url, source_extrait)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
date_echeance = excluded.date_echeance,
|
||||
libelle = excluded.libelle,
|
||||
description = excluded.description,
|
||||
concerne_guadeloupe = excluded.concerne_guadeloupe,
|
||||
note_guadeloupe = excluded.note_guadeloupe,
|
||||
previsionnel = excluded.previsionnel,
|
||||
source_url = COALESCE(excluded.source_url, source_url),
|
||||
source_extrait = COALESCE(excluded.source_extrait, source_extrait)
|
||||
""",
|
||||
(
|
||||
echeance.code,
|
||||
echeance.date_echeance.isoformat(),
|
||||
echeance.libelle,
|
||||
echeance.description,
|
||||
"texte" if texte_id else "nationale",
|
||||
texte_id,
|
||||
None if echeance.concerne_guadeloupe is None else int(echeance.concerne_guadeloupe),
|
||||
echeance.note_guadeloupe,
|
||||
int(echeance.previsionnel),
|
||||
echeance.source_url,
|
||||
echeance.source_extrait,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def enregistrer_insight(cx: sqlite3.Connection, insight: Insight) -> None:
|
||||
cx.execute(
|
||||
"""
|
||||
@@ -332,6 +366,7 @@ def statistiques(cx: sqlite3.Connection) -> dict[str, Any]:
|
||||
"evenements": un("SELECT COUNT(*) FROM evenements"),
|
||||
"decisions_cc": un("SELECT COUNT(*) FROM decisions_cc"),
|
||||
"insights": un("SELECT COUNT(*) FROM insights"),
|
||||
"echeances": un("SELECT COUNT(*) FROM echeances"),
|
||||
"textes_sans_source": un(
|
||||
"SELECT COUNT(*) FROM textes t "
|
||||
"WHERE NOT EXISTS (SELECT 1 FROM sources s WHERE s.texte_id = t.id)"
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Ventilation des impacts sur les libertés par public.
|
||||
|
||||
Le rapport ne dresse pas de tableau « texte × public ». En revanche il consacre
|
||||
un chapitre à chacun des trois publics :
|
||||
|
||||
=========== ===============================================
|
||||
Chapitre Public
|
||||
=========== ===============================================
|
||||
`sec04` Libertés individuelles : analyse croisée
|
||||
`sec05` Libertés associatives : le chantier latent
|
||||
`sec06` Entreprises : obligations nouvelles et ouvertures
|
||||
=========== ===============================================
|
||||
|
||||
Un texte développé dans le chapitre 5 est donc, par construction du rapport, un
|
||||
texte qui touche les libertés associatives. C'est ce placement — un choix
|
||||
éditorial explicite, pas une inférence — qui sert de base à la ventilation.
|
||||
|
||||
Le **sens** de l'impact vient d'abord de la cotation « statut libertés » de
|
||||
l'annexe, seule donnée cotée à la main. À défaut — les textes non promulgués ne
|
||||
sont pas cotés — il est déduit du vocabulaire de la phrase, et le texte est
|
||||
alors marqué « à vérifier ».
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pipeline.modeles import Impact, Public, RegimeLibertes, SensImpact
|
||||
from pipeline.parseurs.chapitres import LectureChapitres
|
||||
from pipeline.parseurs.dates_fr import sans_accents
|
||||
|
||||
CHAPITRE_PAR_PUBLIC: dict[str, Public] = {
|
||||
"sec04": Public.INDIVIDUS,
|
||||
"sec05": Public.ASSOCIATIONS,
|
||||
"sec06": Public.ENTREPRISES,
|
||||
}
|
||||
|
||||
SENS_PAR_REGIME: dict[RegimeLibertes, SensImpact] = {
|
||||
RegimeLibertes.DROITS_PLUS: SensImpact.POSITIF,
|
||||
RegimeLibertes.CONTROLE_PLUS: SensImpact.NEGATIF,
|
||||
RegimeLibertes.MIXTE: SensImpact.MIXTE,
|
||||
RegimeLibertes.NEUTRE: SensImpact.NEUTRE,
|
||||
}
|
||||
|
||||
# Vocabulaire de restriction et d'extension de droits, tel qu'employé par le
|
||||
# rapport. Les listes sont volontairement courtes : un mot ambigu ferait plus
|
||||
# de dégâts qu'un impact manquant.
|
||||
_RESTRICTION = (
|
||||
"restriction", "restreint", "sanction", "amende", "interdiction", "interdit",
|
||||
"peine", "penal", "controle prefectoral", "surveillance", "fichage",
|
||||
"dissolution", "retrait", "suspension", "obligation declarative", "censure",
|
||||
"expropriation", "retention", "fouille", "reduit", "durci", "aggrave",
|
||||
)
|
||||
_EXTENSION = (
|
||||
"droit opposable", "nouveau droit", "garantie", "protection", "reparation",
|
||||
"extension des droits", "acces", "recours", "assistance", "indemnisation",
|
||||
"simplification", "allegement", "ouverture", "facilite", "reserve aux pme",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Ventilation:
|
||||
"""Impacts déduits pour un texte, et ce qui les a motivés."""
|
||||
|
||||
impacts: dict[Public, Impact]
|
||||
deduit: bool # vrai si le sens n'a pas pu venir de la cotation manuelle
|
||||
|
||||
|
||||
MOTIF_VERIFICATION = (
|
||||
"Sens de l'impact déduit du vocabulaire du rapport : l'annexe ne cote le "
|
||||
"régime de libertés que pour les lois promulguées."
|
||||
)
|
||||
|
||||
|
||||
def ventiler(
|
||||
texte_id: str,
|
||||
lecture: LectureChapitres,
|
||||
*,
|
||||
regime: RegimeLibertes | None,
|
||||
) -> Ventilation:
|
||||
"""Répartit l'effet d'un texte entre individus, associations et entreprises."""
|
||||
impacts: dict[Public, Impact] = {}
|
||||
deduit = False
|
||||
|
||||
for mention in lecture.mentions.get(texte_id, []):
|
||||
public = CHAPITRE_PAR_PUBLIC.get(mention.chapitre)
|
||||
if public is None or public in impacts:
|
||||
continue
|
||||
|
||||
phrases = " ".join(mention.phrases)
|
||||
if regime is not None:
|
||||
sens = SENS_PAR_REGIME[regime]
|
||||
else:
|
||||
sens = _sens_du_vocabulaire(phrases)
|
||||
deduit = True
|
||||
|
||||
impacts[public] = Impact(sens=sens, note=_note(mention.phrases))
|
||||
|
||||
return Ventilation(impacts=impacts, deduit=deduit and bool(impacts))
|
||||
|
||||
|
||||
def _sens_du_vocabulaire(fragment: str) -> SensImpact:
|
||||
plat = sans_accents(fragment).lower()
|
||||
restrictions = sum(1 for mot in _RESTRICTION if _present(mot, plat))
|
||||
extensions = sum(1 for mot in _EXTENSION if _present(mot, plat))
|
||||
|
||||
if restrictions and extensions:
|
||||
return SensImpact.MIXTE
|
||||
if restrictions:
|
||||
return SensImpact.NEGATIF
|
||||
if extensions:
|
||||
return SensImpact.POSITIF
|
||||
return SensImpact.NEUTRE
|
||||
|
||||
|
||||
def _present(expression: str, plat: str) -> bool:
|
||||
return re.search(rf"(?<!\w){re.escape(expression)}\w{{0,3}}(?!\w)", plat) is not None
|
||||
|
||||
|
||||
def _note(phrases: list[str]) -> str:
|
||||
"""Première phrase substantielle du chapitre, comme justification affichable."""
|
||||
from pipeline.parseurs.markdown import nettoyer
|
||||
|
||||
for phrase in phrases:
|
||||
propre = nettoyer(phrase)
|
||||
if len(propre) > 60:
|
||||
return propre[:400]
|
||||
return ""
|
||||
@@ -0,0 +1,46 @@
|
||||
-- ============================================================================
|
||||
-- Migration 004 — échéances du calendrier
|
||||
--
|
||||
-- La page « calendrier » agrège trois natures d'échéance :
|
||||
--
|
||||
-- * celles qui appartiennent à un texte (décision attendue du Conseil
|
||||
-- constitutionnel, entrée en vigueur, prochaine étape) — dérivées de
|
||||
-- `textes`, `evenements` et `decisions_cc` ;
|
||||
-- * celles qui n'appartiennent à aucun texte mais structurent la fenêtre de
|
||||
-- veille : sénatoriales du 27 septembre, présentation du projet de loi de
|
||||
-- finances 2027, rentrée parlementaire ordinaire du 1ᵉʳ octobre.
|
||||
--
|
||||
-- Cette table porte les secondes. Le champ `concerne_guadeloupe` est explicite
|
||||
-- et non déductible : le scrutin sénatorial du 27 septembre 2026 ne concerne
|
||||
-- PAS la Guadeloupe (série 1, renouvelée en 2023), ce que le §7 du cahier des
|
||||
-- charges impose de ne pas se tromper.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE echeances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE, -- « senatoriales-2026 »
|
||||
date_echeance TEXT NOT NULL,
|
||||
libelle TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- `nationale` : elle vaut pour l'ensemble du territoire ;
|
||||
-- `texte` : elle découle d'un texte suivi (FK renseignée).
|
||||
portee TEXT NOT NULL DEFAULT 'nationale'
|
||||
CHECK (portee IN ('nationale', 'texte')),
|
||||
texte_id TEXT REFERENCES textes (id) ON DELETE CASCADE,
|
||||
|
||||
-- 1 = concerne la Guadeloupe, 0 = ne la concerne pas, NULL = non tranché.
|
||||
concerne_guadeloupe INTEGER CHECK (concerne_guadeloupe IN (0, 1)),
|
||||
note_guadeloupe TEXT,
|
||||
|
||||
-- Vrai tant que l'échéance est prévisionnelle (postérieure à la date
|
||||
-- d'arrêté du corpus et non confirmée par un collecteur).
|
||||
previsionnel INTEGER NOT NULL DEFAULT 1 CHECK (previsionnel IN (0, 1)),
|
||||
|
||||
source_url TEXT,
|
||||
source_extrait TEXT,
|
||||
cree_le TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_echeances_date ON echeances (date_echeance);
|
||||
CREATE INDEX idx_echeances_texte ON echeances (texte_id);
|
||||
@@ -136,7 +136,13 @@ def parser() -> LectureChapitres:
|
||||
nom = f"sec{numero:02d}"
|
||||
|
||||
for section in decouper_sections(fichier.read_text(encoding="utf-8"), niveau_max=4):
|
||||
cibles_du_titre = _textes_designes(section.titre)
|
||||
nommes_au_titre = _textes_designes(section.titre)
|
||||
# Un titre qui nomme plusieurs textes — « 3.2.3 Cohésion
|
||||
# républicaine, logement, GloBE, entrisme » — n'est dédié à aucun.
|
||||
# Sans cette restriction, chaque phrase de transition de la section
|
||||
# (« Septembre 2026 sera un mois réglementaire… ») atterrirait dans
|
||||
# le résumé des quatre textes à la fois.
|
||||
cibles_du_titre = nommes_au_titre if len(nommes_au_titre) == 1 else []
|
||||
|
||||
for paragraphe in _paragraphes(section.corps):
|
||||
for phrase in _phrases(paragraphe):
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"""Échéances nationales de la fenêtre de veille.
|
||||
|
||||
Le calendrier de l'application mêle des échéances propres à chaque texte —
|
||||
déduites de la base — et un petit nombre d'échéances nationales qui structurent
|
||||
la période et n'appartiennent à aucun texte : décisions attendues du Conseil
|
||||
constitutionnel, bascules du 1ᵉʳ septembre, sénatoriales du 27 septembre,
|
||||
projet de loi de finances pour 2027, rentrée parlementaire.
|
||||
|
||||
Ces échéances sont **transcrites** du corpus, pas déduites : chacune porte le
|
||||
fichier et l'extrait dont elle provient. Deux d'entre elles font l'objet d'une
|
||||
vigilance particulière.
|
||||
|
||||
- Les sénatoriales du 27 septembre 2026 **ne concernent pas la Guadeloupe**.
|
||||
Le décret n° 2026-301 du 21 avril 2026 convoque la série 2 plus la Guyane, la
|
||||
Polynésie, Saint-Barthélemy, Saint-Martin et Wallis-et-Futuna ; la Guadeloupe
|
||||
relève de la série 1, renouvelée le 24 septembre 2023. Le §7 du cahier des
|
||||
charges en fait un critère d'acceptation.
|
||||
- L'interdiction des réseaux sociaux aux moins de 15 ans au 1ᵉʳ septembre est
|
||||
**conditionnelle** : elle dépend de la décision n° 2026-911 DC attendue fin
|
||||
août. Elle est marquée prévisionnelle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Echeance:
|
||||
"""Une échéance nationale de la fenêtre de veille."""
|
||||
|
||||
code: str
|
||||
date_echeance: date
|
||||
libelle: str
|
||||
description: str
|
||||
concerne_guadeloupe: bool | None = None
|
||||
note_guadeloupe: str | None = None
|
||||
previsionnel: bool = True
|
||||
source_url: str | None = None
|
||||
source_extrait: str | None = None
|
||||
|
||||
|
||||
# Transcription du corpus. Chaque entrée cite le fichier dont elle provient.
|
||||
ECHEANCES_NATIONALES: tuple[Echeance, ...] = (
|
||||
# ── research/lois-2026_dim11.md §5 — entrées en vigueur au 1ᵉʳ septembre ──
|
||||
Echeance(
|
||||
code="facturation-electronique-reception",
|
||||
date_echeance=date(2026, 9, 1),
|
||||
libelle="Réception obligatoire des factures électroniques",
|
||||
description=(
|
||||
"Toute entreprise et toute association assujettie à la TVA, franchise en "
|
||||
"base comprise, doit être en capacité de recevoir des factures "
|
||||
"électroniques. Les grandes entreprises et les entreprises de taille "
|
||||
"intermédiaire doivent en outre en émettre ; les PME, TPE et "
|
||||
"micro-entreprises au 1ᵉʳ septembre 2027."
|
||||
),
|
||||
concerne_guadeloupe=True,
|
||||
note_guadeloupe=(
|
||||
"Applicable de plein droit en Guadeloupe, en Martinique et à La Réunion, "
|
||||
"aux taux de TVA locaux. Hors champ en Guyane et à Mayotte, où la TVA "
|
||||
"n'est provisoirement pas applicable. La préparation locale documentée se "
|
||||
"limite à une réunion de la direction régionale des finances publiques "
|
||||
"début juin 2026."
|
||||
),
|
||||
previsionnel=False,
|
||||
source_url="https://www.economie.gouv.fr/facturation-electronique-entreprises",
|
||||
source_extrait=(
|
||||
"À partir du 1er septembre 2026, toutes les entreprises devront être en "
|
||||
"capacité de recevoir des factures électroniques."
|
||||
),
|
||||
),
|
||||
Echeance(
|
||||
code="assurance-chomage-rupture-conventionnelle",
|
||||
date_echeance=date(2026, 9, 1),
|
||||
libelle="Réduction des durées d'indemnisation après rupture conventionnelle",
|
||||
description=(
|
||||
"La durée maximale d'indemnisation après rupture conventionnelle passe à "
|
||||
"15 mois avant 55 ans et 20,5 mois à partir de 55 ans (loi n° 2026-470 du "
|
||||
"11 juin 2026). Des textes d'application restaient attendus au 25 juillet."
|
||||
),
|
||||
concerne_guadeloupe=True,
|
||||
note_guadeloupe=(
|
||||
"Le régime dérogatoire des départements d'outre-mer est préservé : 20 et "
|
||||
"30 mois. La Guadeloupe compte 15,2 % de chômage et 53 780 demandeurs "
|
||||
"d'emploi, dont 56 % de longue durée."
|
||||
),
|
||||
previsionnel=False,
|
||||
source_url="https://www.service-public.fr/particuliers/actualites/A17812",
|
||||
source_extrait=(
|
||||
"La loi portant transposition de l'avenant n° 3 du 25 février 2026 modifie "
|
||||
"la durée maximale de versement des allocations chômage aux salariés en cas "
|
||||
"de rupture conventionnelle."
|
||||
),
|
||||
),
|
||||
Echeance(
|
||||
code="malus-textile-mode-ultra-express",
|
||||
date_echeance=date(2026, 9, 1),
|
||||
libelle="Malus « mode ultra-express » sur les produits textiles",
|
||||
description=(
|
||||
"Pénalité de 0,25 € à 12 € par produit en 2026, portée à 2–20 € en 2030 et "
|
||||
"plafonnée à 50 % du prix hors taxes (loi n° 2026-602 du 8 juillet 2026, "
|
||||
"art. 5). L'arrêté d'application était en consultation publique depuis le "
|
||||
"8 juillet."
|
||||
),
|
||||
concerne_guadeloupe=True,
|
||||
note_guadeloupe="Applicable dans les départements d'outre-mer.",
|
||||
previsionnel=False,
|
||||
source_url="https://www.vie-publique.fr/loi/295986-loi-fast-fashion-mode-ephemere",
|
||||
source_extrait=(
|
||||
"Ce texte a pour objet de permettre l'entrée en vigueur, le 1er septembre "
|
||||
"2026, d'un dispositif de pénalités financières applicables aux produits de "
|
||||
"la mode ultra express."
|
||||
),
|
||||
),
|
||||
Echeance(
|
||||
code="seuils-concentration-releves",
|
||||
date_echeance=date(2026, 9, 1),
|
||||
libelle="Relèvement des seuils de contrôle des concentrations",
|
||||
description=(
|
||||
"Nouveaux seuils de notification à l'Autorité de la concurrence : 250 M€ de "
|
||||
"chiffre d'affaires mondial et 80 M€ en France, contre 150 et 50 ; commerce "
|
||||
"de détail 100 M€ et 20 M€, contre 75 et 15 (loi n° 2026-403, art. 24)."
|
||||
),
|
||||
concerne_guadeloupe=None,
|
||||
previsionnel=False,
|
||||
source_url="https://www.autoritedelaconcurrence.fr/fr/controle-des-concentrations",
|
||||
source_extrait=(
|
||||
"Ces nouveaux seuils s'appliqueront aux opérations qui seront réalisées à "
|
||||
"compter du 1er septembre 2026."
|
||||
),
|
||||
),
|
||||
Echeance(
|
||||
code="reseaux-sociaux-moins-de-15-ans",
|
||||
date_echeance=date(2026, 9, 1),
|
||||
libelle="Interdiction des réseaux sociaux aux moins de 15 ans (sous condition)",
|
||||
description=(
|
||||
"Entrée en vigueur prévue pour les nouveaux comptes, mais suspendue à la "
|
||||
"décision n° 2026-911 DC attendue fin août 2026, et à l'articulation avec "
|
||||
"le règlement européen sur les services numériques — la Commission "
|
||||
"européenne a rendu un avis partiellement défavorable le 6 juillet 2026."
|
||||
),
|
||||
concerne_guadeloupe=True,
|
||||
note_guadeloupe="Applicable de plein droit.",
|
||||
previsionnel=True,
|
||||
source_url="https://www.conseil-constitutionnel.fr/decisions/affaires-en-instances",
|
||||
source_extrait=None,
|
||||
),
|
||||
# ── research/lois-2026_phase5_validation.md, point 4 ─────────────────────
|
||||
Echeance(
|
||||
code="senatoriales-2026",
|
||||
date_echeance=date(2026, 9, 27),
|
||||
libelle="Élections sénatoriales (série 2)",
|
||||
description=(
|
||||
"Le décret n° 2026-301 du 21 avril 2026 convoque les collèges électoraux "
|
||||
"des départements de la série 2, ainsi que la Guyane, la Polynésie "
|
||||
"française, Saint-Barthélemy, Saint-Martin et Wallis-et-Futuna, plus six "
|
||||
"des douze sièges des Français établis hors de France."
|
||||
),
|
||||
concerne_guadeloupe=False,
|
||||
note_guadeloupe=(
|
||||
"La Guadeloupe n'est PAS concernée : elle relève de la série 1, renouvelée "
|
||||
"le 24 septembre 2023. Prochain renouvellement en 2029."
|
||||
),
|
||||
previsionnel=True,
|
||||
source_url=(
|
||||
"https://www.haute-corse.gouv.fr/Actions-de-l-Etat/Vie-democratique/"
|
||||
"Elections/Mairies-Decret-de-convocation-election-senatoriale-2026"
|
||||
),
|
||||
source_extrait=(
|
||||
"Le décret n° 2026-301 du 21 avril 2026 portant convocation des collèges "
|
||||
"électoraux fixe au dimanche 27 septembre 2026 la date des élections "
|
||||
"sénatoriales pour les départements de la série 2 figurant au tableau n° 5 "
|
||||
"annexé au code électoral, ainsi qu'en Guyane, en Polynésie française, à "
|
||||
"Saint-Barthélemy, à Saint-Martin et à Wallis-et-Futuna."
|
||||
),
|
||||
),
|
||||
# ── research/lois-2026_dim11.md §2 et §3 ─────────────────────────────────
|
||||
Echeance(
|
||||
code="rentree-parlementaire-ordinaire",
|
||||
date_echeance=date(2026, 10, 1),
|
||||
libelle="Ouverture de la session ordinaire 2026-2027",
|
||||
description=(
|
||||
"Aucune session extraordinaire de septembre n'était annoncée au "
|
||||
"25 juillet 2026. Les navettes renvoyées à l'automne — pacte migration, "
|
||||
"protection des enfants, cohésion républicaine, logement, entrisme, "
|
||||
"adaptation du droit des outre-mer — reprennent à cette date."
|
||||
),
|
||||
concerne_guadeloupe=None,
|
||||
previsionnel=True,
|
||||
source_url="https://www.assemblee-nationale.fr/dyn/agenda",
|
||||
source_extrait=None,
|
||||
),
|
||||
Echeance(
|
||||
code="plf-2027",
|
||||
date_echeance=date(2026, 10, 1),
|
||||
libelle="Présentation du projet de loi de finances pour 2027",
|
||||
description=(
|
||||
"Présentation en conseil des ministres fin septembre ou début octobre 2026, "
|
||||
"avec 3 milliards d'euros d'économies supplémentaires annoncés. L'élection "
|
||||
"présidentielle du printemps 2027 comprime le calendrier d'examen."
|
||||
),
|
||||
concerne_guadeloupe=None,
|
||||
previsionnel=True,
|
||||
source_url="https://www.vie-publique.fr/loi-finances",
|
||||
source_extrait=None,
|
||||
),
|
||||
# ── lois-france-2026-guadeloupe_sec10.md, conclusion ─────────────────────
|
||||
Echeance(
|
||||
code="prochaine-date-arrete",
|
||||
date_echeance=date(2026, 8, 24),
|
||||
libelle="Fin du délai d'examen des saisines du 24 juillet",
|
||||
description=(
|
||||
"Terme du délai constitutionnel d'un mois pour les saisines enregistrées le "
|
||||
"24 juillet 2026 — loi « Riposte » (2026-915 DC), urgence agricole "
|
||||
"(2026-914 DC), patrimoine immobilier (2026-913 DC), réseaux sociaux "
|
||||
"(2026-911 DC). C'est la prochaine date d'arrêté utile de la veille."
|
||||
),
|
||||
concerne_guadeloupe=True,
|
||||
note_guadeloupe=(
|
||||
"Trois textes à enjeu maximal pour les libertés y sont suspendus, dont la "
|
||||
"loi « Riposte » et ses fouilles sans réquisition jusqu'à 40 km du "
|
||||
"littoral — soit la quasi-totalité de la Guadeloupe."
|
||||
),
|
||||
previsionnel=True,
|
||||
source_url="https://www.conseil-constitutionnel.fr/decisions/affaires-en-instances",
|
||||
source_extrait=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def echeances_nationales() -> tuple[Echeance, ...]:
|
||||
return ECHEANCES_NATIONALES
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Arbitrages entre sources divergentes.
|
||||
|
||||
Deux mécanismes distincts :
|
||||
|
||||
1. **Les cinq verdicts de la validation de phase 5** — des désaccords déjà
|
||||
tranchés sur sources primaires, que le seed doit refléter. Le §7 du cahier
|
||||
des charges en fait des critères d'acceptation : la loi « Riposte » doit
|
||||
porter l'amende forfaitaire à **500 €** et non 1 500 € (montant abandonné
|
||||
après la première lecture au Sénat), et la Guadeloupe ne doit **pas**
|
||||
figurer parmi les territoires concernés par les sénatoriales de 2026.
|
||||
|
||||
2. **La hiérarchie des sources** pour le pipeline de mise à jour : Journal
|
||||
officiel et Légifrance priment sur l'Assemblée et le Sénat, qui priment sur
|
||||
le Conseil constitutionnel, qui prime sur la presse. Déclarée dans
|
||||
`pipeline/config.yaml`, appliquée par `update.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
|
||||
from pipeline import chemins
|
||||
from pipeline.journal import logger
|
||||
from pipeline.parseurs.markdown import extraire_tableaux, nettoyer
|
||||
|
||||
log = logger("conflits")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 1. Verdicts de la validation de phase 5
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@dataclass(slots=True)
|
||||
class Verdict:
|
||||
"""Un désaccord tranché sur source primaire."""
|
||||
|
||||
numero: int
|
||||
objet: str
|
||||
statut: str # « TRANCHÉ »
|
||||
reponse: str
|
||||
textes_concernes: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def point_cle(self) -> str:
|
||||
return f"Arbitrage (validation du 25 juillet 2026) : {self.reponse}"
|
||||
|
||||
|
||||
# Rattachement de chaque verdict aux textes qu'il concerne. La table est
|
||||
# déclarative et non devinée : chaque ligne cite l'objet du désaccord tel qu'il
|
||||
# figure dans `lois-2026_phase5_validation.md`.
|
||||
TEXTES_PAR_VERDICT: dict[int, list[str]] = {
|
||||
# « AFD "participation" free party : 500 € (A) ou 1 500 € (B) ? »
|
||||
1: ["loi-riposte"],
|
||||
# « AFD "inhalation" N₂O : 200 € ou 500 € ? »
|
||||
2: ["loi-riposte"],
|
||||
# « Saisine CC réellement déposée au 25/07 ? Loi promulguée ? »
|
||||
3: ["loi-riposte"],
|
||||
# « Guadeloupe concernée par les sénatoriales du 27/09/2026 ? »
|
||||
# Aucun texte de la base : l'arbitrage porte sur une échéance du calendrier.
|
||||
4: [],
|
||||
# « Autres lois promulguées entre le 21 et le 25/07/2026 ? »
|
||||
# Porte sur l'exhaustivité de l'inventaire, pas sur un texte en particulier.
|
||||
5: [],
|
||||
}
|
||||
|
||||
|
||||
def lire_verdicts(markdown: str) -> list[Verdict]:
|
||||
"""Lit la synthèse des verdicts de la validation de phase 5."""
|
||||
verdicts: list[Verdict] = []
|
||||
|
||||
for tableau in extraire_tableaux(markdown):
|
||||
if not tableau.entetes or tableau.entetes[0] != "#":
|
||||
continue
|
||||
for ligne in tableau.lignes:
|
||||
numero_brut = nettoyer(ligne["#"])
|
||||
if not numero_brut.isdigit():
|
||||
continue
|
||||
numero = int(numero_brut)
|
||||
verdicts.append(
|
||||
Verdict(
|
||||
numero=numero,
|
||||
objet=nettoyer(ligne.get("Objet du désaccord", "")),
|
||||
statut=nettoyer(ligne.get("Verdict", "")),
|
||||
reponse=nettoyer(ligne.get("Réponse tranchée", "")),
|
||||
textes_concernes=TEXTES_PAR_VERDICT.get(numero, []),
|
||||
)
|
||||
)
|
||||
|
||||
log.info("verdicts de validation lus", verdicts=len(verdicts))
|
||||
return verdicts
|
||||
|
||||
|
||||
# Précisions chiffrées à porter en points clés, reprises verbatim des claims
|
||||
# décisifs de la validation. Elles répondent au §7 du cahier des charges, qui
|
||||
# exige de retrouver « AFD 500 € » sur la loi « Riposte ».
|
||||
PRECISIONS_RIPOSTE: tuple[str, ...] = (
|
||||
"Participation à une free party non autorisée : 6 mois d'emprisonnement et "
|
||||
"7 500 € d'amende ; amende forfaitaire délictuelle (AFD) de 500 € "
|
||||
"(minorée 400 €, majorée 1 000 €) — art. L. 211-15-4 du code de la "
|
||||
"sécurité intérieure. Le montant de 1 500 € était la version votée par le "
|
||||
"Sénat en première lecture, abandonnée.",
|
||||
"Inhalation de protoxyde d'azote : 1 an d'emprisonnement et 3 750 € "
|
||||
"d'amende ; AFD de 500 € — art. L. 3611-4 du code de la santé publique.",
|
||||
"Autres amendes forfaitaires délictuelles du texte : détention et transport "
|
||||
"500 €, vente illicite 800 €.",
|
||||
)
|
||||
|
||||
PRECISIONS_PAR_TEXTE: dict[str, tuple[str, ...]] = {
|
||||
"loi-riposte": PRECISIONS_RIPOSTE,
|
||||
}
|
||||
|
||||
|
||||
def appliquer(textes: dict, verdicts: list[Verdict]) -> int:
|
||||
"""Porte les arbitrages dans les points clés des textes concernés.
|
||||
|
||||
Retourne le nombre de textes modifiés.
|
||||
"""
|
||||
modifies = 0
|
||||
|
||||
for verdict in verdicts:
|
||||
for texte_id in verdict.textes_concernes:
|
||||
texte = textes.get(texte_id)
|
||||
if texte is None:
|
||||
log.warning("verdict orphelin", verdict=verdict.numero, texte=texte_id)
|
||||
continue
|
||||
if verdict.point_cle not in texte.points_cles:
|
||||
texte.points_cles.append(verdict.point_cle)
|
||||
modifies += 1
|
||||
|
||||
for texte_id, precisions in PRECISIONS_PAR_TEXTE.items():
|
||||
texte = textes.get(texte_id)
|
||||
if texte is None:
|
||||
continue
|
||||
for precision in precisions:
|
||||
if precision not in texte.points_cles:
|
||||
texte.points_cles.append(precision)
|
||||
|
||||
return modifies
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 2. Hiérarchie des sources pour la mise à jour
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _hierarchie() -> list[str]:
|
||||
configuration = yaml.safe_load(chemins.CONFIG.read_text(encoding="utf-8"))
|
||||
return list(configuration.get("hierarchie_sources", []))
|
||||
|
||||
|
||||
def rang_de_source(url: str) -> int:
|
||||
"""Rang d'autorité d'une URL : plus il est bas, plus la source prime.
|
||||
|
||||
Les hôtes inconnus — presse et analyses — reçoivent le rang le plus faible.
|
||||
"""
|
||||
hote = (urlparse(url).hostname or "").lower().removeprefix("www.")
|
||||
for rang, reference in enumerate(_hierarchie()):
|
||||
if hote == reference or hote.endswith("." + reference):
|
||||
return rang
|
||||
return len(_hierarchie())
|
||||
|
||||
|
||||
def trancher(candidats: list[tuple[str, str]]) -> str | None:
|
||||
"""Choisit une valeur parmi des couples (valeur, url) divergents.
|
||||
|
||||
La valeur retenue est celle qu'affirme la source la plus autorisée. À
|
||||
autorité égale, la valeur majoritaire l'emporte ; à égalité parfaite, aucune
|
||||
n'est retenue — le pipeline signale plutôt que de trancher au hasard.
|
||||
"""
|
||||
if not candidats:
|
||||
return None
|
||||
|
||||
par_valeur: dict[str, tuple[int, int]] = {}
|
||||
for valeur, url in candidats:
|
||||
rang = rang_de_source(url)
|
||||
meilleur_rang, occurrences = par_valeur.get(valeur, (len(_hierarchie()) + 1, 0))
|
||||
par_valeur[valeur] = (min(meilleur_rang, rang), occurrences + 1)
|
||||
|
||||
classement = sorted(par_valeur.items(), key=lambda item: (item[1][0], -item[1][1]))
|
||||
if len(classement) > 1 and classement[0][1] == classement[1][1]:
|
||||
return None
|
||||
return classement[0][0]
|
||||
|
||||
|
||||
_MONTANT = re.compile(r"(\d[\d\s ]*)\s*(?:€|euros)", re.IGNORECASE)
|
||||
|
||||
|
||||
def montants_cites(fragment: str) -> list[int]:
|
||||
"""Montants en euros cités dans un fragment, pour les contrôles d'acceptation."""
|
||||
montants: list[int] = []
|
||||
for brut in _MONTANT.findall(fragment):
|
||||
chiffres = re.sub(r"\D", "", brut)
|
||||
if chiffres:
|
||||
montants.append(int(chiffres))
|
||||
return montants
|
||||
@@ -24,7 +24,7 @@ import sqlite3
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from pipeline import chemins, db, guadeloupe
|
||||
from pipeline import chemins, db, guadeloupe, impacts, resolution_conflits
|
||||
from pipeline.journal import configurer, logger
|
||||
from pipeline.modeles import Source, Texte
|
||||
from pipeline.parseurs import (
|
||||
@@ -32,6 +32,7 @@ from pipeline.parseurs import (
|
||||
bibliographie,
|
||||
chapitres,
|
||||
citations,
|
||||
echeances,
|
||||
tableaux_sec10,
|
||||
)
|
||||
|
||||
@@ -50,10 +51,13 @@ class CompteRendu:
|
||||
evenements: int = 0
|
||||
decisions_cc: int = 0
|
||||
analyses: int = 0
|
||||
echeances: 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)
|
||||
impacts_deduits: list[str] = field(default_factory=list)
|
||||
verdicts_appliques: int = 0
|
||||
avertissements: list[str] = field(default_factory=list)
|
||||
|
||||
marqueurs_avec_lien: int = 0
|
||||
@@ -69,6 +73,7 @@ class CompteRendu:
|
||||
f" événements : {self.evenements}",
|
||||
f" décisions CC : {self.decisions_cc}",
|
||||
f" analyses : {self.analyses}",
|
||||
f" échéances : {self.echeances}",
|
||||
"",
|
||||
"Résolution des citations",
|
||||
f" avec lien : {self.marqueurs_avec_lien}",
|
||||
@@ -81,6 +86,8 @@ class CompteRendu:
|
||||
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)}",
|
||||
f" impacts déduits : {len(self.impacts_deduits)}",
|
||||
f" arbitrages appliqués : {self.verdicts_appliques}",
|
||||
]
|
||||
if self.avertissements:
|
||||
lignes.append("")
|
||||
@@ -114,6 +121,7 @@ def ensemencer(cx: sqlite3.Connection) -> CompteRendu:
|
||||
_rattacher_sources(texte, repertoire, lecture, marqueurs_annexe)
|
||||
_rattacher_redaction(texte, lecture)
|
||||
_coter_guadeloupe(texte, compte)
|
||||
_ventiler_impacts(texte, lecture, compte)
|
||||
|
||||
if not texte.sources:
|
||||
compte.textes_sans_source.append(texte.id)
|
||||
@@ -130,13 +138,22 @@ def ensemencer(cx: sqlite3.Connection) -> CompteRendu:
|
||||
compte.marqueurs_sans_lien = len(repertoire.resolus_sans_lien)
|
||||
compte.marqueurs_non_resolus = len(repertoire.non_resolus)
|
||||
|
||||
# 5. Écriture.
|
||||
# 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.
|
||||
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)
|
||||
for echeance in echeances.echeances_nationales():
|
||||
db.enregistrer_echeance(cx, echeance)
|
||||
|
||||
statistiques = db.statistiques(cx)
|
||||
compte.textes = statistiques["textes"]
|
||||
@@ -144,6 +161,7 @@ def ensemencer(cx: sqlite3.Connection) -> CompteRendu:
|
||||
compte.evenements = statistiques["evenements"]
|
||||
compte.decisions_cc = statistiques["decisions_cc"]
|
||||
compte.analyses = statistiques["insights"]
|
||||
compte.echeances = statistiques["echeances"]
|
||||
|
||||
if compte.textes < TEXTES_ATTENDUS_MINIMUM:
|
||||
compte.avertissements.append(
|
||||
@@ -234,6 +252,23 @@ def _coter_guadeloupe(texte: Texte, compte: CompteRendu) -> None:
|
||||
compte.pertinences_deduites.append(texte.id)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _ajouter_motif(existant: str | None, nouveau: str) -> str:
|
||||
if not existant:
|
||||
return nouveau
|
||||
|
||||
Reference in New Issue
Block a user