"""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"(? 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 ""