"""Détection de la pertinence guadeloupéenne et ultramarine d'un texte. Le tableau A de l'annexe cote lui-même les 27 lois promulguées ; le tableau B ne cote pas les textes en cours. Pour ces derniers, la cotation est déduite du vocabulaire employé par le rapport, avec des poids déclarés dans `pipeline/config.yaml`. Toute cotation obtenue ainsi est signalée : le texte porte `a_verifier = 1` et un motif explicite. L'interface l'affiche comme telle. Une cotation déduite n'est pas une cotation établie. """ from __future__ import annotations import functools import re from dataclasses import dataclass, field import yaml from pipeline import chemins from pipeline.modeles import Pertinence from pipeline.parseurs.dates_fr import sans_accents # Tournures par lesquelles le corpus **écarte** un territoire qu'il vient de # nommer : « PPL montagne — sans portée pour la Guadeloupe », « PJL logement — # pas de volet outre-mer à ce stade ». Compter la mention sans lire la négation # donnerait à ces textes la cotation la plus forte, à l'exact inverse de ce que # la source affirme. NEGATIONS = ( "sans portee", "sans objet", "sans incidence", "sans specificite", "sans volet", "sans consequence", "ne concerne pas", "n'est pas concerne", "pas concernee", "pas de volet", "pas d'incidence", "non concerne", "hors champ", "exclue", "exclus", ) # Fenêtre de voisinage, en caractères, autour d'une mention de territoire. _AVANT = 90 _APRES = 60 @dataclass(slots=True) class Cotation: """Résultat d'une détection, avec ce qui l'a motivée.""" pertinence: Pertinence | None score: int expressions: list[str] exclusions: list[str] = field(default_factory=list) @property def note(self) -> str | None: if self.exclusions and not self.expressions: return ( "Le rapport écarte explicitement ce territoire : " + ", ".join(f"« {e} »" for e in self.exclusions[:3]) ) if not self.expressions: return None note = "Détecté d'après : " + ", ".join(self.expressions[:6]) if self.exclusions: note += " — mais le rapport écarte par ailleurs : " + ", ".join( f"« {e} »" for e in self.exclusions[:2] ) return note @functools.lru_cache(maxsize=1) def _configuration() -> dict: return yaml.safe_load(chemins.CONFIG.read_text(encoding="utf-8")) @functools.lru_cache(maxsize=1) def _motifs() -> list[tuple[re.Pattern[str], str, int]]: """Compile un motif par expression, ancré sur des frontières de mot.""" expressions = _configuration()["guadeloupe"]["expressions"] compiles: list[tuple[re.Pattern[str], str, int]] = [] for expression, poids in expressions.items(): plat = sans_accents(str(expression)).lower() compiles.append((re.compile(rf"(? Cotation: """Cote la pertinence guadeloupéenne d'un ensemble de fragments de texte. Une mention niée ne compte pas. « Sans portée pour la Guadeloupe » nomme le territoire pour l'écarter : la lire comme une pertinence forte inverserait le sens de la source. """ plat = sans_accents(" ".join(f for f in fragments if f)).lower() if not plat.strip(): return Cotation(pertinence=None, score=0, expressions=[]) score = 0 trouvees: list[str] = [] exclusions: list[str] = [] for motif, expression, poids in _motifs(): positions = [m.start() for m in motif.finditer(plat)] if not positions: continue retenues = [p for p in positions if not _est_niee(plat, p)] if retenues: score += poids trouvees.append(expression) else: exclusions.append(_extrait_autour(plat, positions[0])) seuils = _configuration()["guadeloupe"]["seuils"] if score >= seuils["forte"]: pertinence = Pertinence.FORTE elif score >= seuils["moyenne"]: pertinence = Pertinence.MOYENNE elif score >= seuils["faible"]: pertinence = Pertinence.FAIBLE elif exclusions: # Le territoire est nommé, mais pour être écarté : la pertinence est # faible, et le motif dit pourquoi. pertinence = Pertinence.FAIBLE else: pertinence = None return Cotation( pertinence=pertinence, score=score, expressions=trouvees, exclusions=exclusions ) def _est_niee(plat: str, position: int) -> bool: """Vrai si une tournure d'exclusion entoure la mention trouvée.""" voisinage = plat[max(0, position - _AVANT) : position + _APRES] return any(negation in voisinage for negation in NEGATIONS) def _extrait_autour(plat: str, position: int, largeur: int = 70) -> str: debut = max(0, position - largeur) fin = min(len(plat), position + largeur) return " ".join(plat[debut:fin].split()) MOTIF_VERIFICATION = ( "Pertinence Guadeloupe déduite automatiquement : l'annexe du rapport ne " "cote que les lois promulguées, pas les textes encore en discussion." )