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:
@@ -57,3 +57,45 @@ def test_returns_sha256_hex():
|
||||
sig = trajectory_signature([{"action_type": "mouse_click", "target": "x"}])
|
||||
assert len(sig) == 64
|
||||
assert all(c in "0123456789abcdef" for c in sig)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# R1/R2 amendés — verdict Qwen 2026-06-25 : normalisation déterministe + PII
|
||||
# neutralisée par regex DÉDIÉES (pas de pii_blur, pas de NER). Stabilité
|
||||
# labo/DGX = portabilité de la signature. Noms sans titre : stratégie (b)
|
||||
# (impact 0 en labo, gate = audit agrégat DGX avant prod).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_target_normalized_case_and_accents():
|
||||
"""Q2 : casse et accents ne changent pas la signature (même cible sémantique)."""
|
||||
a = [{"action_type": "mouse_click", "target": "Valider"}]
|
||||
b = [{"action_type": "mouse_click", "target": "VALIDER"}]
|
||||
c = [{"action_type": "mouse_click", "target": "validér"}]
|
||||
assert trajectory_signature(a) == trajectory_signature(b) == trajectory_signature(c)
|
||||
|
||||
|
||||
def test_pii_ipp_neutralized():
|
||||
"""R1 : deux IPP différents sur le même champ → MÊME signature (PII neutralisée).
|
||||
Et une cible sans identifiant reste discriminée."""
|
||||
a = [{"action_type": "mouse_click", "target": "Patient IPP 25012257"}]
|
||||
b = [{"action_type": "mouse_click", "target": "Patient IPP 30045678"}]
|
||||
assert trajectory_signature(a) == trajectory_signature(b)
|
||||
c = [{"action_type": "mouse_click", "target": "Patient liste"}]
|
||||
assert trajectory_signature(a) != trajectory_signature(c)
|
||||
|
||||
|
||||
def test_pii_date_neutralized():
|
||||
"""R1 : deux dates différentes → MÊME signature."""
|
||||
a = [{"action_type": "mouse_click", "target": "RDV du 12/05/2026"}]
|
||||
b = [{"action_type": "mouse_click", "target": "RDV du 03/11/2025"}]
|
||||
assert trajectory_signature(a) == trajectory_signature(b)
|
||||
|
||||
|
||||
def test_pii_phone_and_email_neutralized():
|
||||
"""R1 : téléphone (FR) et email neutralisés (deux valeurs distinctes → même sig)."""
|
||||
tel_a = [{"action_type": "text_input", "target": "tel 06 12 34 56 78"}]
|
||||
tel_b = [{"action_type": "text_input", "target": "tel 07 98 76 54 32"}]
|
||||
assert trajectory_signature(tel_a) == trajectory_signature(tel_b)
|
||||
mail_a = [{"action_type": "text_input", "target": "mail jean.dupont@chu.fr"}]
|
||||
mail_b = [{"action_type": "text_input", "target": "mail m.martin@chu.fr"}]
|
||||
assert trajectory_signature(mail_a) == trajectory_signature(mail_b)
|
||||
|
||||
Reference in New Issue
Block a user