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>
130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
"""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 ""
|