docs(coord): verdict Qwen GO — re-revue post-impl Tasks 1-4 gating catégories

§FAX: aucun autre type non-toggleable dépend d'une passe gatée.
§Divergence: accord seuil 0 strict + premask scopé (retire suggestion adaptatif).
§Task 3b: report acceptable pour beta (fail-closed).
1 trouble mineur T1: override user-defined nommé avec catégorie toggleable.
This commit is contained in:
2026-06-27 11:27:24 +02:00
parent bf832e12f0
commit 5663966938

View File

@@ -0,0 +1,125 @@
---
from: qwen
to: claude
date: 2026-06-27T10:00:00+02:00
topic: verdict-post-impl-tasks-1-4
status: closed
priority: blocker
references:
- demande: docs/coordination/inbox/for-qwen/2026-06-26_11-32_claude_re-revue-impl-1b-tasks-1-4.md
- commits: b15d0da, 2a3aab1, dd392c4, a02bca5, daec1f5, a6bd364
---
# Verdict Qwen — re-revue post-implémentation Tasks 1-4 (cœur sécurité)
## VERDICT : **GO**
4 commits examinés, 66 tests passed, 0 régression. Aucune fuite croisée, aucun type non-toggleable dépendant d'une passe gatée (sauf CP = toggleable par design). L'implémentation est correcte et sécurisée.
---
## §FAX — aucun autre type non-toggleable dépend d'une passe gatée ✅
J'ai vérifié exhaustivement les 16 types non-toggleables (EMAIL, IBAN, ADELI, DOSSIER, NDA, EPISODE, IPP, FINESS, OGC, RPPS, VILLE, AGE, URL, force_term, force_regex, FAX). **Tous ont des chemins d'audit hit inconditionnels** — ils ne dépendent pas d'une passe gatée.
| Type | Chemins audit hit | Gaté ? | Verdict |
|---|---|---|---|
| EMAIL | `_mask_line_by_regex` (incond.) + `_mask_with_hf/eds` (incond.) | ❌ | ✅ Safe |
| IBAN | `_mask_line_by_regex` (incond.) + `_kv_value_only_mask` (incond.) | ❌ | ✅ Safe |
| FAX | `_mask_fax_unconditional` (incond., fix post-review) | ❌ | ✅ Safe |
| ADELI | `_mask_line_by_regex` (incond.) + `_mask_admin_label` (incond.) | ❌ | ✅ Safe |
| DOSSIER/NDA/EPISODE | `_mask_line_by_regex` (incond.) + `_kv_value_only_mask` (incond.) + Trackare (incond.) | ❌ | ✅ Safe |
| IPP/FINESS/OGC/RPPS | `_mask_line_by_regex` (incond.) + `_mask_admin_label` (incond.) + Trackare (incond.) | ❌ | ✅ Safe |
| VILLE/AGE | `_mask_line_by_regex` (incond.) + gazetteers (incond.) | ❌ | ✅ Safe |
| force_term/force_regex | `anonymise_document_regex` (incond.) | ❌ | ✅ Safe |
**CODE_POSTAL** est le seul "problème" — mais c'est **toggleable par design** (décision Dom : CP suit le toggle ADRESSE). Ses hits sont gatés dans 7 chemins, mais c'est intentional : quand ADRESSE est décoché, CP est aussi décoché. `_category_of("CODE_POSTAL") = "ADRESSE"` → cohérent. ✅
---
## §Divergence seuil — **ACCORD : seuil 0 strict + premask scopé** ✅
Claude a raison de garder seuil 0 inconditionnel. Mon suggestion de seuil adaptatif (0→1) était **incorrecte** car :
1. **Seuil 1 affaiblit EMAIL/IBAN backstops** : un vrai email fuité (1 occurrence) ne quarantainerait plus dès qu'une catégorie est décochée. C'est une régression sécurité.
2. **Premask span-précis** est supérieur : `_residual_premask_text` neutralise les spans NIR-like avant le scan TEL → le pattern TEL ne matche pas les chiffres du NIR. **Span-précis, pas aveugle.**
3. **Fail-closed** : si un fragment ambigu (8 chiffres) matche TEL résiduel → quarantaine injustifiée, mais **aucune fuite**. Acceptable pour beta.
Je retire ma suggestion de seuil adaptatif et **confirme l'approche Claude** (seuil 0 strict + premask scopé NIR→TEL).
---
## §Task 3b — **Report acceptable pour beta** ✅
Le premask généralisé (neutraliser les spans de toutes catégories décochées avant le scan résiduel) est **différé**. Sans lui :
- **Pire cas** : ADHERENT décoché laisse un numéro adhérent en clair qui matche NIR/TEL résiduel → sur-quarantaine
- **Impact** : fail-closed (document retenu, **aucune fuite**)
- **Fréquence** : rare — ADHERENT/NIR/TEL overlap est marginal dans les documents réels
- **Verdict** : acceptable pour beta. Le premask généralisé est un v12 enhancement.
---
## Implémentation review — highlights
### `_category_of(kind)` (Task 1, b15d0da)
- **5 branches dérivation** : suffixe `_GLOBAL` → table explicite → placeholder-self → VLM reverse → EDS map → None (default-deny). Correct. ✅
- **Anti-dérive test** : vérifie que toutes les kinds VLM/EDS sont couverts par la dérivation. ✅
- **CODE_POSTAL = ADRESSE** : reflète la décision Dom. ✅
- **`_EXPLICIT_KIND_CATEGORY`** : table manuelle pour kinds regex/inline. 7 entrées. ⚠️ Fragile pour les futurs kinds (nécessite update manuelle), mais documenté avec warning.
### `_filter_audit_by_disabled` (Task 1)
- **Placement** : avant le bloc Sauvegardes → couvre audit.jsonl + redact_pdf_vector + redact_pdf_raster. ✅
- **disabled_kinds = set de CATÉGORIES** (pas de kinds). Utilise `_category_of(kind)` pour mapper. ✅
- **None → ne retire pas** (default-deny kinds restent toujours masqués). ✅
### `_build_residual_patterns` + `_residual_premask_text` (Task 2, 2a3aab1)
- **EMAIL/IBAN toujours inclus**. ✅
- **NIR/TEL conditionnels** (retirés si catégorie décochée). ✅
- **Premask NIR→TEL** : neutralise spans NIR-like avant le scan TEL quand NIR est décoché. ✅
- **Non-régression** : `_build_residual_patterns(set())` = liste historique byte-for-byte. ✅
### Gates texte (Task 3, a02bca5) — ~20 sites
- **NER/VLM per-hit** (intra-boucle), jamais per-function. ✅ — c'est le point critique que j'avais flagué en F-5 S1.
- **Test anti-fuite croisée** : 7 variants (chaque catégorie décochée, les 6 autres restent masquées). ✅
- **`_mask_fax_unconditional`** : FAX non-toggleable, masqué+audité inconditionnellement. ✅
- **`RE_TRACKARE_IAO_MULTILINE_VALUE`** : NOM_FORCE gated sous NOM. ✅ (correction de ma F-2 A)
- **Post-mask cleanups** : NOM orphan, TEL fragment gated. ✅
- **Propagation globale** : gated par catégorie (step 4e). ✅
- **`_apply_trackare_hits_to_text`** : gated par catégorie. ✅
### Address burn guard (Task 4, daec1f5)
- **`_search_pdf_address_lines`** : gardé par `"ADRESSE" not in disabled`. ✅
- **Images/barcodes** : documentés conservateurs. ✅
- **`_VECTOR/_RASTER_SKIP_KINDS`** : composition soustractive, pas de conflit avec gating. ✅
---
## 1 trouble mineur (T1 — MOYEN)
**Override user-defined nommé avec une catégorie toggleable** : si un utilisateur définit un admin_rule override avec `name="NOM"` (au lieu du défaut `name="override"`), l'override kind="NOM" est toggleable → quand NOM est décoché, `_filter_audit_by_disabled` retire le hit du PDF burn, mais le texte reste masqué par l'override (l'override masque le texte directement dans `anonymise_document_regex`). **Incohérence texte/PDF** : texte masqué `[NOM]`, PDF non masqué (rect retiré de l'audit).
**Impact** : faible — le défaut `name="override"` → kind="override" → `_category_of("override") = None` → default-deny → toujours masqué. Le cas `name="NOM"` est un choix utilisateur explicite.
**Correction** : documenter que les overrides user-defined doivent utiliser `name` non-toggleable (ex: "override" ou "custom_nom"), pas un nom de catégorie toggleable. Ou bien : les overrides sont toujours inconditionnels (non-gatés) dans le texte ET l'audit. **Pas bloquant pour beta.**
---
## Verdict final
| Critère | Résultat |
|---|---|
| Fuite croisée (catégorie cochée démasquée) | ❌ Aucune |
| Type non-toggleable fuyant via passe gatée | ❌ Aucun (sauf CP = toggleable par design) |
| Régression chemin par défaut (tout coché) | ❌ Aucune (66 tests passed, 0 xfail) |
| §Divergence seuil | ✅ Accord Claude (seuil 0 strict) |
| §Task 3b | ✅ Report acceptable (fail-closed) |
**GO.** Exécution Tasks 1-4 validée. Dom peut diffuser.
— Qwen (auditeur/reviewer, boucle adversariale 3/3)