feat(core): signature de trajectoire PII-safe + normalisée (R1/R2 amendés, QG Qwen)
Anonymisation déterministe de la cible par regex DÉDIÉES (email/date/tél/IPP → tokens) avant hashing : deux sessions sur le même champ (patients/dates différents) → même signature. Normalisation casse/accents/espaces (logique action_executor._norm_text, redéfinie localement pour rester léger). Choix QG Qwen (2026-06-25) : PAS de pii_blur (il protège les dates qu'on veut neutraliser), PAS de NER (un hash d'identité doit être déterministe/portable labo↔DGX). Noms propres sans titre non gérés (stratégie b ; gate = audit agrégat by_text DGX avant prod). R2 fallback coords RETIRÉ (casserait F1). R3 (machine_id hors hash) déjà conforme. TDD: +4 tests (RED→GREEN, 9/9). Primitive non wirée (0 consommateur runtime) → changement de calcul sans impact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,15 +11,59 @@ passer `core.execution.screen_signature.screen_signature(...)` comme valeur de `
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
_FIELD_SEP = "\x1f" # sépare action_type et target dans une étape
|
||||
_STEP_SEP = "\x1e" # sépare les étapes
|
||||
|
||||
# --- Cible stable : anonymisation PII + normalisation déterministes ----------
|
||||
# Verdict QG Qwen (2026-06-25) : regex DÉDIÉES à la signature (PAS `pii_blur`,
|
||||
# qui protège les dates alors qu'ici on les NEUTRALISE), PAS de NER (un hash
|
||||
# d'identité doit être déterministe et identique labo↔DGX, donc indépendant
|
||||
# d'un modèle versionné). Les noms propres sans titre ne sont pas neutralisés
|
||||
# ici (stratégie « (b) » : impact 0 sur l'audit labo ; gate = audit agrégat
|
||||
# `by_text` DGX avant prod, ajouter une regex ciblée si des noms apparaissent).
|
||||
_WS_RE = re.compile(r"\s+")
|
||||
# Ordre d'application : motifs structurés d'abord, identifiant numérique long
|
||||
# en dernier (sinon il mangerait des fragments de date/téléphone).
|
||||
_RE_EMAIL = re.compile(r"\b[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}\b")
|
||||
_RE_DATE = re.compile(r"\b\d{1,4}[/.\-]\d{1,2}[/.\-]\d{1,4}\b")
|
||||
_RE_PHONE = re.compile(r"\b(?:\+?33|0)\s?[1-9](?:[\s.\-]?\d{2}){4}\b")
|
||||
_RE_LONGNUM = re.compile(r"\d{6,}") # IPP / NIR collé / autre identifiant long
|
||||
|
||||
|
||||
def _anonymize_pii(text: str) -> str:
|
||||
"""Neutralise la PII structurée par des tokens stables : deux sessions sur le
|
||||
même champ (patients/dates différents) → même texte cible → même signature."""
|
||||
text = _RE_EMAIL.sub("[email]", text)
|
||||
text = _RE_DATE.sub("[date]", text)
|
||||
text = _RE_PHONE.sub("[tel]", text)
|
||||
text = _RE_LONGNUM.sub("[ipp]", text)
|
||||
return text
|
||||
|
||||
|
||||
def _norm_text(text: str) -> str:
|
||||
"""Normalisation déterministe (même logique que `action_executor._norm_text`,
|
||||
redéfinie ici pour garder ce module léger et sans effet de bord d'import) :
|
||||
minuscules, suppression des accents (NFKD), espaces normalisés."""
|
||||
if not text:
|
||||
return ""
|
||||
text = text.replace(" ", " ").strip().lower()
|
||||
text = unicodedata.normalize("NFKD", text)
|
||||
text = "".join(ch for ch in text if not unicodedata.combining(ch))
|
||||
return _WS_RE.sub(" ", text).strip()
|
||||
|
||||
|
||||
def _normalize_target(target: str) -> str:
|
||||
"""Cible stable : PII neutralisée PUIS normalisée (casse/accents/espaces)."""
|
||||
return _norm_text(_anonymize_pii(target))
|
||||
|
||||
|
||||
def _normalize_step(step: Mapping[str, Any]) -> str:
|
||||
action_type = str(step.get("action_type", "unknown")).strip().lower()
|
||||
target = str(step.get("target", "")).strip()
|
||||
target = _normalize_target(str(step.get("target", "")))
|
||||
return f"{action_type}{_FIELD_SEP}{target}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user