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:
Cyber Mawonaj
2026-07-25 20:23:51 -04:00
co-authored by Claude Opus 5
parent c28243cf87
commit f3e546cfc4
8 changed files with 922 additions and 3 deletions
+129
View File
@@ -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 ""