Compare commits
47 Commits
feature/q1
...
a827d860f1
| Author | SHA1 | Date | |
|---|---|---|---|
| a827d860f1 | |||
| eb14cd219d | |||
| c9572c383a | |||
| 274e2fa586 | |||
| 7a2af5c905 | |||
| 4488a1d4a0 | |||
| 19e089ea38 | |||
| 26b210607c | |||
| 6e0e8c7312 | |||
| 26ac02b0cb | |||
| 782551c1c6 | |||
| 8629a0cda0 | |||
| e967a67052 | |||
| bc2fe667a0 | |||
| f9532d5543 | |||
| 4e6fd97e84 | |||
| cede2d64d6 | |||
| 98a21d7ccc | |||
| ea761823d6 | |||
| 47a71df930 | |||
| 93617bab55 | |||
| dfa6e2957b | |||
| eb797a4761 | |||
| 85e19af655 | |||
| d6915247fe | |||
| bf30f622d9 | |||
| b46ea83900 | |||
| 5163cb1657 | |||
| 09231be5e8 | |||
| 3b1f6cdfbe | |||
| 78adb3ba70 | |||
| 63bd4ace1d | |||
| ee34042179 | |||
| 883f14ab79 | |||
| f92da4d54e | |||
| 871221ea56 | |||
| f188116bc1 | |||
| 6806aee587 | |||
| 70ff0b9e12 | |||
| dfa45041d7 | |||
| 4eba826ca5 | |||
| 0ba5424eb0 | |||
| 99b6e7f1d1 | |||
| 30a6ebcc19 | |||
| f61e767ee6 | |||
| c78f9f415d | |||
| 340348b820 |
50
.gitignore
vendored
50
.gitignore
vendored
@@ -6,12 +6,10 @@ __pycache__/
|
|||||||
*.egg
|
*.egg
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
release/
|
|
||||||
*.whl
|
*.whl
|
||||||
|
|
||||||
# === Virtual environments ===
|
# === Virtual environments ===
|
||||||
.venv/
|
.venv/
|
||||||
.venv_build_win/
|
|
||||||
venv/
|
venv/
|
||||||
venv_*/
|
venv_*/
|
||||||
env/
|
env/
|
||||||
@@ -42,17 +40,6 @@ models/
|
|||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.gif
|
*.gif
|
||||||
# Exception : assets embarqués dans l'exe (splash, icônes…) doivent être versionnés
|
|
||||||
!assets/**
|
|
||||||
!assets
|
|
||||||
|
|
||||||
# build_info.py : régénéré automatiquement par scripts/rebuild_anon.ps1
|
|
||||||
# avec date/commit/branch. Ne pas versionner.
|
|
||||||
build_info.py
|
|
||||||
|
|
||||||
# gui_v6/_build_version.py : généré au build Windows par build_windows_oneclick.ps1
|
|
||||||
# (contient BUILD_VERSION = "2026.MM.JJ.HHMM"). Ne pas commiter.
|
|
||||||
gui_v6/_build_version.py
|
|
||||||
*.mp3
|
*.mp3
|
||||||
*.wav
|
*.wav
|
||||||
*.mp4
|
*.mp4
|
||||||
@@ -72,9 +59,6 @@ Thumbs.db
|
|||||||
# === Secrets ===
|
# === Secrets ===
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
*.pfx
|
|
||||||
*.p12
|
|
||||||
build_signing.local.ps1
|
|
||||||
credentials.json
|
credentials.json
|
||||||
token.pickle
|
token.pickle
|
||||||
|
|
||||||
@@ -90,37 +74,3 @@ htmlcov/
|
|||||||
# === Backups ===
|
# === Backups ===
|
||||||
*_backup_*
|
*_backup_*
|
||||||
backups/
|
backups/
|
||||||
|
|
||||||
# === RGPD : corpus réels et annotations contenant des PII ===
|
|
||||||
# Exclure les répertoires de travail contenant des données réelles patient
|
|
||||||
corpus_validation/
|
|
||||||
corpus_validation_sample/
|
|
||||||
test_chcb_leak/
|
|
||||||
test_force_term_leak/
|
|
||||||
test_3ogc/
|
|
||||||
test_anonymise/
|
|
||||||
test_gui_output/
|
|
||||||
data/silver_annotations/*.bio
|
|
||||||
regression_tests/baseline/
|
|
||||||
tests/ground_truth/pdfs/
|
|
||||||
tests/ground_truth/annotations/
|
|
||||||
tests/phase1_production_test/
|
|
||||||
|
|
||||||
# === RGPD : sorties de pseudonymisation contenant potentiellement des PII ===
|
|
||||||
pdf_natif/
|
|
||||||
ano/pdf_natif/pseudonymise/
|
|
||||||
|
|
||||||
# === Mode admin local ===
|
|
||||||
.admin
|
|
||||||
|
|
||||||
# === Agents IA : caches et artefacts de session ===
|
|
||||||
.claude/
|
|
||||||
.codex-loop/
|
|
||||||
.qwen/
|
|
||||||
|
|
||||||
# === Artefacts graphify (knowledge graph généré) ===
|
|
||||||
graphify-out/
|
|
||||||
|
|
||||||
# Sorties d'anonymisation avec PII en clair (RGPD) — ne jamais committer
|
|
||||||
*.audit.jsonl
|
|
||||||
*.pseudonymise.txt
|
|
||||||
|
|||||||
@@ -122,9 +122,8 @@ Fonction : `_mask_line_by_regex`
|
|||||||
| Dates | `[DATE]` | 12/03/2024 |
|
| Dates | `[DATE]` | 12/03/2024 |
|
||||||
| Adresses | `[ADRESSE]` | 12 rue de la Paix |
|
| Adresses | `[ADRESSE]` | 12 rue de la Paix |
|
||||||
|
|
||||||
Configuration :
|
Configuration supplementaire via `config/dictionnaires.yml` :
|
||||||
- `config/dictionnaires.default.yml` : template versionne, source de verite des valeurs par defaut
|
listes blanches, force-mask et regex personnalisees.
|
||||||
- `config/dictionnaires.yml` : surcharge locale chargee par defaut, contenant uniquement les ecarts site/runtime
|
|
||||||
|
|
||||||
### 3. Reconnaissance d'entites nommees (NER)
|
### 3. Reconnaissance d'entites nommees (NER)
|
||||||
|
|
||||||
@@ -181,7 +180,6 @@ un fallback OCR est utilise :
|
|||||||
|
|
||||||
| Element | Description |
|
| Element | Description |
|
||||||
|-------------------------------|------------------------------------------------|
|
|-------------------------------|------------------------------------------------|
|
||||||
| `config/dictionnaires.default.yml` | Valeurs par defaut completes et versionnees |
|
| `config/dictionnaires.yml` | Listes blanches, force-mask, regex custom |
|
||||||
| `config/dictionnaires.yml` | Surcharge locale optionnelle (ecarts uniquement) |
|
|
||||||
| `Pseudonymisation_Gui_V5.py` | Interface graphique (traitement par lots) |
|
| `Pseudonymisation_Gui_V5.py` | Interface graphique (traitement par lots) |
|
||||||
| Ligne de commande | `python anonymizer_core_refactored_onnx.py fichier.pdf --hf --raster` |
|
| Ligne de commande | `python anonymizer_core_refactored_onnx.py fichier.pdf --hf --raster` |
|
||||||
|
|||||||
@@ -48,16 +48,33 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
yaml = None
|
yaml = None
|
||||||
|
|
||||||
from config_defaults import (
|
|
||||||
RUNTIME_DICTIONARIES_CONFIG_PATH,
|
|
||||||
read_default_dictionaries_text,
|
|
||||||
read_runtime_dictionaries_overlay_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
APP_TITLE = "Pseudonymisation de PDF"
|
APP_TITLE = "Pseudonymisation de PDF"
|
||||||
DEFAULT_CFG = RUNTIME_DICTIONARIES_CONFIG_PATH
|
DEFAULT_CFG = Path("config/dictionnaires.yml")
|
||||||
DEFAULTS_CFG_TEXT = read_default_dictionaries_text()
|
|
||||||
RUNTIME_CFG_TEXT = read_runtime_dictionaries_overlay_text()
|
DEFAULTS_CFG_TEXT = r"""
|
||||||
|
# dictionnaires.yml – valeurs par défaut (bloc littéral pour les regex)
|
||||||
|
version: 1
|
||||||
|
encoding: "utf-8"
|
||||||
|
normalization: "NFKC"
|
||||||
|
whitelist:
|
||||||
|
sections_titres: [DIM, GHM, GHS, RUM, COMPTE, RENDU, DIAGNOSTIC]
|
||||||
|
noms_maj_excepts: ["Médecin DIM", "Praticien conseil"]
|
||||||
|
org_gpe_keep: true
|
||||||
|
blacklist:
|
||||||
|
force_mask_terms: []
|
||||||
|
force_mask_regex: []
|
||||||
|
kv_labels_preserve: [FINESS, IPP, "N° OGC", Etablissement]
|
||||||
|
regex_overrides:
|
||||||
|
- name: OGC_court
|
||||||
|
pattern: |-
|
||||||
|
\b(?:N°\s*)?OGC\s*[:\-]?\s*([A-Za-z0-9\-]{1,3})\b
|
||||||
|
placeholder: '[OGC]'
|
||||||
|
flags: [IGNORECASE]
|
||||||
|
flags:
|
||||||
|
case_insensitive: true
|
||||||
|
unicode_word_boundaries: true
|
||||||
|
regex_engine: "python"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ToolTip:
|
class ToolTip:
|
||||||
@@ -191,7 +208,7 @@ class App:
|
|||||||
# YAML helpers
|
# YAML helpers
|
||||||
def _ensure_cfg_exists(self):
|
def _ensure_cfg_exists(self):
|
||||||
p = Path(self.cfg_path.get()); p.parent.mkdir(parents=True, exist_ok=True)
|
p = Path(self.cfg_path.get()); p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if not p.exists(): p.write_text(RUNTIME_CFG_TEXT, encoding="utf-8")
|
if not p.exists(): p.write_text(DEFAULTS_CFG_TEXT, encoding="utf-8")
|
||||||
def _cfg_browse(self):
|
def _cfg_browse(self):
|
||||||
d = filedialog.asksaveasfilename(defaultextension=".yml", filetypes=[("YAML","*.yml *.yaml"), ("Tous","*.*")])
|
d = filedialog.asksaveasfilename(defaultextension=".yml", filetypes=[("YAML","*.yml *.yaml"), ("Tous","*.*")])
|
||||||
if d: self.cfg_path.set(d)
|
if d: self.cfg_path.set(d)
|
||||||
@@ -208,14 +225,14 @@ class App:
|
|||||||
if yaml is None:
|
if yaml is None:
|
||||||
messagebox.showerror("PyYAML manquant", "Installez PyYAML (pip install pyyaml)."); return
|
messagebox.showerror("PyYAML manquant", "Installez PyYAML (pip install pyyaml)."); return
|
||||||
try:
|
try:
|
||||||
Path(self.cfg_path.get()).write_text(yaml.safe_dump(self.cfg_data or {}, allow_unicode=True, sort_keys=False), encoding="utf-8")
|
Path(self.cfg_path.get()).write_text(yaml.safe_dump(self.cfg_data or yaml.safe_load(DEFAULTS_CFG_TEXT), allow_unicode=True, sort_keys=False), encoding="utf-8")
|
||||||
self._log("Règles sauvegardées.")
|
self._log("Règles sauvegardées.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Erreur", f"Impossible d'écrire le YAML: {e}")
|
messagebox.showerror("Erreur", f"Impossible d'écrire le YAML: {e}")
|
||||||
def _reload_cfg(self): self._load_cfg(); self._log("Règles rechargées.")
|
def _reload_cfg(self): self._load_cfg(); self._log("Règles rechargées.")
|
||||||
def _restore_defaults(self):
|
def _restore_defaults(self):
|
||||||
try:
|
try:
|
||||||
Path(self.cfg_path.get()).write_text(RUNTIME_CFG_TEXT, encoding="utf-8"); self._log("Surcharge locale réinitialisée."); self._load_cfg()
|
Path(self.cfg_path.get()).write_text(DEFAULTS_CFG_TEXT, encoding="utf-8"); self._log("CFG par défaut écrit."); self._load_cfg()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Erreur", f"Impossible d'écrire le YAML par défaut: {e}")
|
messagebox.showerror("Erreur", f"Impossible d'écrire le YAML par défaut: {e}")
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Point d'entrée de la GUI V6 de Pseudonymisation.
|
|
||||||
|
|
||||||
Usage :
|
|
||||||
python Pseudonymisation_Gui_V6.py # lance la fenêtre
|
|
||||||
python Pseudonymisation_Gui_V6.py --self-test # importe l'app, sort 0, sans fenêtre
|
|
||||||
|
|
||||||
Le mode ``--self-test`` vérifie que tout le socle GUI V6 s'importe correctement
|
|
||||||
(utile en CI / build sans display). Il n'ouvre aucune fenêtre.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Frozen Windows : désactiver le manager ONNX legacy AVANT tout import du cœur,
|
|
||||||
# pour éviter « cannot load module more than once per process » (hotfix CLI 6c6f653).
|
|
||||||
os.environ.setdefault("ANON_SKIP_LEGACY_ONNX_MANAGER", "1")
|
|
||||||
|
|
||||||
|
|
||||||
def _self_test() -> int:
|
|
||||||
"""Importe les modules du socle GUI V6 sans créer de fenêtre."""
|
|
||||||
from gui_v6 import ( # noqa: F401
|
|
||||||
app,
|
|
||||||
config_state,
|
|
||||||
engine_bridge,
|
|
||||||
license_client,
|
|
||||||
license_store,
|
|
||||||
machine_id,
|
|
||||||
processing_runner,
|
|
||||||
theme,
|
|
||||||
ui_kit,
|
|
||||||
)
|
|
||||||
from gui_v6.tabs import tab_about, tab_config, tab_usage # noqa: F401
|
|
||||||
|
|
||||||
# Sanity check des contrats publics du socle.
|
|
||||||
assert hasattr(app, "AnonymisationApp")
|
|
||||||
assert hasattr(license_client, "LicenseClient")
|
|
||||||
assert hasattr(license_client, "LicenseStatus")
|
|
||||||
assert hasattr(license_store, "LicenseStore")
|
|
||||||
assert hasattr(processing_runner, "ProcessingRunner")
|
|
||||||
assert hasattr(engine_bridge, "make_process_fn")
|
|
||||||
assert hasattr(config_state, "ConfigState")
|
|
||||||
assert hasattr(machine_id, "default_machine_id")
|
|
||||||
assert hasattr(ui_kit, "Card")
|
|
||||||
assert hasattr(theme, "PALETTES") and set(theme.PALETTES) >= {"sombre", "clair", "medical", "neutre"}
|
|
||||||
assert hasattr(tab_about, "AboutTab")
|
|
||||||
assert hasattr(tab_config, "ConfigTab")
|
|
||||||
assert hasattr(tab_usage, "UsageTab")
|
|
||||||
print("GUI V6 self-test OK")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None) -> int:
|
|
||||||
argv = list(sys.argv[1:] if argv is None else argv)
|
|
||||||
if "--self-test" in argv:
|
|
||||||
return _self_test()
|
|
||||||
|
|
||||||
try:
|
|
||||||
from gui_v6.logging_setup import setup_file_logging
|
|
||||||
|
|
||||||
setup_file_logging()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
from gui_v6.app import AnonymisationApp
|
|
||||||
from gui_v6.single_instance import AlreadyRunningError, SingleInstance
|
|
||||||
|
|
||||||
guard = SingleInstance()
|
|
||||||
try:
|
|
||||||
guard.acquire()
|
|
||||||
except AlreadyRunningError:
|
|
||||||
try:
|
|
||||||
import tkinter.messagebox as mb
|
|
||||||
|
|
||||||
mb.showinfo("Anonymisation", "L'application est déjà ouverte.")
|
|
||||||
except Exception:
|
|
||||||
print("L'application est déjà ouverte.")
|
|
||||||
return 0
|
|
||||||
try:
|
|
||||||
application = AnonymisationApp()
|
|
||||||
application.mainloop()
|
|
||||||
finally:
|
|
||||||
guard.release()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""Mode admin pour l'application Pseudonymisation (D-13).
|
|
||||||
|
|
||||||
Le mode admin déverrouille des fonctionnalités cachées au bêta-testeur :
|
|
||||||
- VLM Ollama (D-11) — détection visuelle par LLM local
|
|
||||||
- Paramètres avancés sensibles (stopwords personnalisés, force_terms, etc.)
|
|
||||||
- Profils techniques (regex_overrides)
|
|
||||||
|
|
||||||
Activation possible (par ordre de priorité) :
|
|
||||||
1. Variable d'environnement : `ANON_ADMIN=1`
|
|
||||||
2. Fichier `.admin` à la racine de l'application (à côté de l'EXE / du module)
|
|
||||||
|
|
||||||
Pour désactiver : supprimer le fichier `.admin` et la variable d'env.
|
|
||||||
|
|
||||||
Aucun mot de passe pour la v1.0 — c'est juste un verrou "interdit aux
|
|
||||||
distraits" qui empêche le bêta-testeur ou un utilisateur final de tomber
|
|
||||||
sur des options qui pourraient leak des données (envoi à Ollama externe,
|
|
||||||
modifications config critique).
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
_ADMIN_CACHED: Optional[bool] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _project_root() -> Path:
|
|
||||||
"""Retourne le dossier racine de l'application (compat dev + EXE)."""
|
|
||||||
try:
|
|
||||||
return Path(__file__).parent.resolve()
|
|
||||||
except NameError:
|
|
||||||
return Path.cwd()
|
|
||||||
|
|
||||||
|
|
||||||
def is_admin(force_refresh: bool = False) -> bool:
|
|
||||||
"""Retourne True si le mode admin est actif.
|
|
||||||
|
|
||||||
Résultat caché en module (les vérifications coûtent presque rien mais
|
|
||||||
`is_admin()` peut être appelé dans des boucles serrées). `force_refresh`
|
|
||||||
permet de re-vérifier après un changement de configuration.
|
|
||||||
"""
|
|
||||||
global _ADMIN_CACHED
|
|
||||||
if _ADMIN_CACHED is not None and not force_refresh:
|
|
||||||
return _ADMIN_CACHED
|
|
||||||
|
|
||||||
# Priorité 1 : variable d'env
|
|
||||||
env_val = os.environ.get("ANON_ADMIN", "").strip().lower()
|
|
||||||
if env_val in ("1", "true", "yes", "on"):
|
|
||||||
_ADMIN_CACHED = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Priorité 2 : fichier .admin
|
|
||||||
admin_file = _project_root() / ".admin"
|
|
||||||
if admin_file.exists():
|
|
||||||
_ADMIN_CACHED = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
_ADMIN_CACHED = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def admin_required(feature_name: str = "fonctionnalité") -> None:
|
|
||||||
"""Lève RuntimeError si pas admin.
|
|
||||||
|
|
||||||
À utiliser comme garde au début d'une méthode sensible.
|
|
||||||
"""
|
|
||||||
if not is_admin():
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Mode admin requis pour {feature_name}. "
|
|
||||||
f"Activez via ANON_ADMIN=1 ou créez le fichier .admin "
|
|
||||||
f"à la racine de l'application."
|
|
||||||
)
|
|
||||||
406
admin_rules.py
406
admin_rules.py
@@ -1,406 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Helpers partagés pour les règles d'administration.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from copy import deepcopy
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
import re
|
|
||||||
|
|
||||||
try:
|
|
||||||
import yaml
|
|
||||||
except Exception:
|
|
||||||
yaml = None
|
|
||||||
|
|
||||||
from config_defaults import CONFIG_DIR, deep_merge_dict
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ADMIN_RULES_CONFIG_PATH = CONFIG_DIR / "admin_rules.default.yml"
|
|
||||||
RUNTIME_ADMIN_RULES_CONFIG_PATH = CONFIG_DIR / "admin_rules.yml"
|
|
||||||
|
|
||||||
_RUNTIME_ADMIN_RULES_OVERLAY_TEXT = """# Surcharge locale des règles d'administration.
|
|
||||||
# Ce fichier est optionnel. Les règles actives de config/admin_rules.default.yml
|
|
||||||
# restent valides tant qu'aucune surcharge locale n'est définie ici.
|
|
||||||
#
|
|
||||||
# Exemple :
|
|
||||||
# version: 1
|
|
||||||
# rules:
|
|
||||||
# - id: rule_identifier_1234567
|
|
||||||
# status: active
|
|
||||||
# governance:
|
|
||||||
# approved_by: responsable_qualite
|
|
||||||
version: 1
|
|
||||||
rules: []
|
|
||||||
"""
|
|
||||||
|
|
||||||
_FALLBACK_DEFAULT_ADMIN_RULES_DICT: dict[str, Any] = {
|
|
||||||
"version": 1,
|
|
||||||
"rules": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _is_non_empty_string(value: Any) -> bool:
|
|
||||||
return isinstance(value, str) and bool(value.strip())
|
|
||||||
|
|
||||||
|
|
||||||
def read_default_admin_rules_text() -> str:
|
|
||||||
try:
|
|
||||||
return DEFAULT_ADMIN_RULES_CONFIG_PATH.read_text(encoding="utf-8")
|
|
||||||
except Exception:
|
|
||||||
return "version: 1\nrules: []\n"
|
|
||||||
|
|
||||||
|
|
||||||
def read_runtime_admin_rules_overlay_text() -> str:
|
|
||||||
return _RUNTIME_ADMIN_RULES_OVERLAY_TEXT
|
|
||||||
|
|
||||||
|
|
||||||
def load_default_admin_rules_dict() -> dict[str, Any]:
|
|
||||||
if yaml is None:
|
|
||||||
return deepcopy(_FALLBACK_DEFAULT_ADMIN_RULES_DICT)
|
|
||||||
try:
|
|
||||||
loaded = yaml.safe_load(read_default_admin_rules_text()) or {}
|
|
||||||
if isinstance(loaded, dict):
|
|
||||||
return loaded
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return deepcopy(_FALLBACK_DEFAULT_ADMIN_RULES_DICT)
|
|
||||||
|
|
||||||
|
|
||||||
def load_runtime_admin_rules_overlay_dict(path: Path | None = None) -> dict[str, Any]:
|
|
||||||
target = Path(path) if path is not None else RUNTIME_ADMIN_RULES_CONFIG_PATH
|
|
||||||
if not target.exists() or yaml is None:
|
|
||||||
return {}
|
|
||||||
try:
|
|
||||||
loaded = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
|
|
||||||
if isinstance(loaded, dict):
|
|
||||||
return loaded
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_rules_by_id(base_rules: list[dict[str, Any]], overlay_rules: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
||||||
merged: list[dict[str, Any]] = [deepcopy(rule) for rule in base_rules]
|
|
||||||
index_by_id = {
|
|
||||||
rule.get("id"): idx
|
|
||||||
for idx, rule in enumerate(merged)
|
|
||||||
if isinstance(rule, dict) and _is_non_empty_string(rule.get("id"))
|
|
||||||
}
|
|
||||||
for overlay_rule in overlay_rules:
|
|
||||||
if not isinstance(overlay_rule, dict):
|
|
||||||
continue
|
|
||||||
rule_id = overlay_rule.get("id")
|
|
||||||
if _is_non_empty_string(rule_id) and rule_id in index_by_id:
|
|
||||||
idx = index_by_id[rule_id]
|
|
||||||
merged[idx] = deep_merge_dict(merged[idx], overlay_rule)
|
|
||||||
else:
|
|
||||||
merged.append(deepcopy(overlay_rule))
|
|
||||||
if _is_non_empty_string(rule_id):
|
|
||||||
index_by_id[rule_id] = len(merged) - 1
|
|
||||||
return merged
|
|
||||||
|
|
||||||
|
|
||||||
def merge_admin_rules_dict(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
merged = deep_merge_dict(base, {k: v for k, v in overlay.items() if k != "rules"})
|
|
||||||
merged["rules"] = _merge_rules_by_id(base.get("rules", []) or [], overlay.get("rules", []) or [])
|
|
||||||
return merged
|
|
||||||
|
|
||||||
|
|
||||||
def load_effective_admin_rules_dict(path: Path | None = None) -> dict[str, Any]:
|
|
||||||
return merge_admin_rules_dict(
|
|
||||||
load_default_admin_rules_dict(),
|
|
||||||
load_runtime_admin_rules_overlay_dict(path),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_runtime_admin_rules_config(path: Path | None = None) -> Path:
|
|
||||||
target = Path(path) if path is not None else RUNTIME_ADMIN_RULES_CONFIG_PATH
|
|
||||||
if not target.exists():
|
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
target.write_text(read_runtime_admin_rules_overlay_text(), encoding="utf-8")
|
|
||||||
return target
|
|
||||||
|
|
||||||
|
|
||||||
def _dedupe_keep_order(values: list[str]) -> list[str]:
|
|
||||||
seen: set[str] = set()
|
|
||||||
output: list[str] = []
|
|
||||||
for value in values:
|
|
||||||
if value in seen:
|
|
||||||
continue
|
|
||||||
seen.add(value)
|
|
||||||
output.append(value)
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def generate_rule_variants(rule: dict[str, Any], limit: int = 12) -> list[str]:
|
|
||||||
rule_type = rule.get("type")
|
|
||||||
match = rule.get("match") or {}
|
|
||||||
normalization = rule.get("normalization") or {}
|
|
||||||
variants: list[str] = []
|
|
||||||
|
|
||||||
if rule_type in {"exact_term", "preserve_phrase"}:
|
|
||||||
exact_value = str(match.get("exact_value", "")).strip()
|
|
||||||
return [exact_value] if exact_value else []
|
|
||||||
|
|
||||||
if rule_type == "normalized_identifier":
|
|
||||||
canonical = str(match.get("canonical_value", "")).strip()
|
|
||||||
prefixes = normalization.get("accepted_prefixes") or []
|
|
||||||
separators = normalization.get("prefix_value_separators") or [" "]
|
|
||||||
if normalization.get("allow_bare_value", False) and canonical:
|
|
||||||
variants.append(canonical)
|
|
||||||
for prefix in prefixes:
|
|
||||||
for separator in separators:
|
|
||||||
variants.append(f"{prefix}{separator}{canonical}")
|
|
||||||
if normalization.get("multiline", False):
|
|
||||||
variants.append(f"{prefix}\n{canonical}")
|
|
||||||
return _dedupe_keep_order(variants)[:limit]
|
|
||||||
|
|
||||||
if rule_type == "contextual_identifier":
|
|
||||||
canonical = str(match.get("canonical_value", "")).strip()
|
|
||||||
prefixes = match.get("context_prefixes") or []
|
|
||||||
separators = match.get("context_separators") or [": ", ":"]
|
|
||||||
for prefix in prefixes:
|
|
||||||
for separator in separators:
|
|
||||||
variants.append(f"{prefix}{separator}{canonical}")
|
|
||||||
if (rule.get("normalization") or {}).get("multiline", False):
|
|
||||||
variants.append(f"{prefix}\n{canonical}")
|
|
||||||
variants.append(f"{prefix} :\n{canonical}")
|
|
||||||
return _dedupe_keep_order(variants)[:limit]
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
VALID_TYPES = {
|
|
||||||
"exact_term",
|
|
||||||
"normalized_identifier",
|
|
||||||
"contextual_identifier",
|
|
||||||
"preserve_phrase",
|
|
||||||
}
|
|
||||||
VALID_ACTIONS = {"mask", "preserve"}
|
|
||||||
VALID_STATUSES = {"draft", "candidate", "approved", "active", "disabled", "retired"}
|
|
||||||
VALID_ENVIRONMENTS = {"test", "staging", "prod"}
|
|
||||||
VALID_SECTIONS = {"narrative", "structured", "table", "header", "footer"}
|
|
||||||
|
|
||||||
|
|
||||||
def validate_rules_config(data: dict[str, Any]) -> list[str]:
|
|
||||||
errors: list[str] = []
|
|
||||||
|
|
||||||
version = data.get("version")
|
|
||||||
if not isinstance(version, int) or version < 1:
|
|
||||||
errors.append("`version` doit etre un entier >= 1.")
|
|
||||||
|
|
||||||
rules = data.get("rules")
|
|
||||||
if not isinstance(rules, list):
|
|
||||||
errors.append("`rules` doit etre une liste.")
|
|
||||||
return errors
|
|
||||||
|
|
||||||
seen_ids: set[str] = set()
|
|
||||||
for index, rule in enumerate(rules):
|
|
||||||
prefix = f"rules[{index}]"
|
|
||||||
if not isinstance(rule, dict):
|
|
||||||
errors.append(f"{prefix}: chaque regle doit etre un mapping.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
rule_id = rule.get("id")
|
|
||||||
if not _is_non_empty_string(rule_id):
|
|
||||||
errors.append(f"{prefix}: `id` est obligatoire.")
|
|
||||||
elif rule_id in seen_ids:
|
|
||||||
errors.append(f"{prefix}: `id` duplique `{rule_id}`.")
|
|
||||||
else:
|
|
||||||
seen_ids.add(rule_id)
|
|
||||||
|
|
||||||
if not _is_non_empty_string(rule.get("label")):
|
|
||||||
errors.append(f"{prefix}: `label` est obligatoire.")
|
|
||||||
|
|
||||||
rule_type = rule.get("type")
|
|
||||||
if rule_type not in VALID_TYPES:
|
|
||||||
errors.append(f"{prefix}: `type` invalide.")
|
|
||||||
|
|
||||||
action = rule.get("action")
|
|
||||||
if action not in VALID_ACTIONS:
|
|
||||||
errors.append(f"{prefix}: `action` invalide.")
|
|
||||||
|
|
||||||
status = rule.get("status")
|
|
||||||
if status not in VALID_STATUSES:
|
|
||||||
errors.append(f"{prefix}: `status` invalide.")
|
|
||||||
|
|
||||||
if action == "mask" and not _is_non_empty_string(rule.get("placeholder")):
|
|
||||||
errors.append(f"{prefix}: `placeholder` est obligatoire pour une regle de masquage.")
|
|
||||||
|
|
||||||
match = rule.get("match")
|
|
||||||
if not isinstance(match, dict):
|
|
||||||
errors.append(f"{prefix}: `match` doit etre un mapping.")
|
|
||||||
match = {}
|
|
||||||
|
|
||||||
normalization = rule.get("normalization") or {}
|
|
||||||
if normalization and not isinstance(normalization, dict):
|
|
||||||
errors.append(f"{prefix}: `normalization` doit etre un mapping.")
|
|
||||||
normalization = {}
|
|
||||||
|
|
||||||
scope = rule.get("scope")
|
|
||||||
if not isinstance(scope, dict):
|
|
||||||
errors.append(f"{prefix}: `scope` doit etre un mapping.")
|
|
||||||
scope = {}
|
|
||||||
|
|
||||||
governance = rule.get("governance")
|
|
||||||
if not isinstance(governance, dict):
|
|
||||||
errors.append(f"{prefix}: `governance` doit etre un mapping.")
|
|
||||||
governance = {}
|
|
||||||
|
|
||||||
document_families = scope.get("document_families")
|
|
||||||
if not isinstance(document_families, list) or not document_families:
|
|
||||||
errors.append(f"{prefix}: `scope.document_families` doit etre une liste non vide.")
|
|
||||||
|
|
||||||
environments = scope.get("environments")
|
|
||||||
if not isinstance(environments, list) or not environments:
|
|
||||||
errors.append(f"{prefix}: `scope.environments` doit etre une liste non vide.")
|
|
||||||
else:
|
|
||||||
invalid_envs = [value for value in environments if value not in VALID_ENVIRONMENTS]
|
|
||||||
if invalid_envs:
|
|
||||||
errors.append(f"{prefix}: environnements invalides: {', '.join(invalid_envs)}.")
|
|
||||||
|
|
||||||
sections = scope.get("sections")
|
|
||||||
if not isinstance(sections, list) or not sections:
|
|
||||||
errors.append(f"{prefix}: `scope.sections` doit etre une liste non vide.")
|
|
||||||
else:
|
|
||||||
invalid_sections = [value for value in sections if value not in VALID_SECTIONS]
|
|
||||||
if invalid_sections:
|
|
||||||
errors.append(f"{prefix}: sections invalides: {', '.join(invalid_sections)}.")
|
|
||||||
|
|
||||||
if not _is_non_empty_string(governance.get("owner")):
|
|
||||||
errors.append(f"{prefix}: `governance.owner` est obligatoire.")
|
|
||||||
if not _is_non_empty_string(governance.get("justification")):
|
|
||||||
errors.append(f"{prefix}: `governance.justification` est obligatoire.")
|
|
||||||
if not _is_non_empty_string(governance.get("created_at")):
|
|
||||||
errors.append(f"{prefix}: `governance.created_at` est obligatoire.")
|
|
||||||
|
|
||||||
tests = governance.get("tests")
|
|
||||||
if not isinstance(tests, dict):
|
|
||||||
errors.append(f"{prefix}: `governance.tests` doit etre un mapping.")
|
|
||||||
tests = {}
|
|
||||||
required_case_ids = tests.get("required_case_ids")
|
|
||||||
if not isinstance(required_case_ids, list) or not required_case_ids:
|
|
||||||
errors.append(f"{prefix}: `governance.tests.required_case_ids` doit etre une liste non vide.")
|
|
||||||
|
|
||||||
if rule_type == "exact_term":
|
|
||||||
if not _is_non_empty_string(match.get("exact_value")):
|
|
||||||
errors.append(f"{prefix}: `match.exact_value` est obligatoire pour `exact_term`.")
|
|
||||||
|
|
||||||
if rule_type == "preserve_phrase":
|
|
||||||
if action != "preserve":
|
|
||||||
errors.append(f"{prefix}: `preserve_phrase` doit utiliser `action: preserve`.")
|
|
||||||
if not _is_non_empty_string(match.get("exact_value")):
|
|
||||||
errors.append(f"{prefix}: `match.exact_value` est obligatoire pour `preserve_phrase`.")
|
|
||||||
|
|
||||||
if rule_type == "normalized_identifier":
|
|
||||||
if not _is_non_empty_string(match.get("canonical_value")):
|
|
||||||
errors.append(f"{prefix}: `match.canonical_value` est obligatoire pour `normalized_identifier`.")
|
|
||||||
|
|
||||||
if rule_type == "contextual_identifier":
|
|
||||||
if not _is_non_empty_string(match.get("canonical_value")):
|
|
||||||
errors.append(f"{prefix}: `match.canonical_value` est obligatoire pour `contextual_identifier`.")
|
|
||||||
context_prefixes = match.get("context_prefixes")
|
|
||||||
if not isinstance(context_prefixes, list) or not context_prefixes:
|
|
||||||
errors.append(f"{prefix}: `match.context_prefixes` doit etre une liste non vide.")
|
|
||||||
|
|
||||||
if status == "active" and governance.get("review_required_for_activation", False):
|
|
||||||
if not _is_non_empty_string(governance.get("approved_by")):
|
|
||||||
errors.append(f"{prefix}: `governance.approved_by` est obligatoire pour une regle active.")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def _placeholder_to_kind(placeholder: str) -> str:
|
|
||||||
if isinstance(placeholder, str) and placeholder.startswith("[") and placeholder.endswith("]"):
|
|
||||||
return placeholder[1:-1]
|
|
||||||
return "MASK"
|
|
||||||
|
|
||||||
|
|
||||||
def _literal_to_pattern(text: str, multiline: bool) -> str:
|
|
||||||
parts: list[str] = []
|
|
||||||
for char in text:
|
|
||||||
if char == " ":
|
|
||||||
parts.append(r"\s*" if multiline else r"[ \t]*")
|
|
||||||
elif char == "\n":
|
|
||||||
parts.append(r"\s*" if multiline else r"\n")
|
|
||||||
else:
|
|
||||||
parts.append(re.escape(char))
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def _compile_identifier_rule(rule: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
rule_type = rule.get("type")
|
|
||||||
normalization = rule.get("normalization") or {}
|
|
||||||
multiline = bool(normalization.get("multiline", False))
|
|
||||||
flags = re.IGNORECASE if normalization.get("case_insensitive", False) else 0
|
|
||||||
value = str((rule.get("match") or {}).get("canonical_value", "")).strip()
|
|
||||||
value_rx = re.escape(value)
|
|
||||||
boundary_before = r"(?<![A-Za-z0-9])"
|
|
||||||
boundary_after = r"(?![A-Za-z0-9])"
|
|
||||||
patterns = []
|
|
||||||
|
|
||||||
if rule_type == "normalized_identifier":
|
|
||||||
if normalization.get("allow_bare_value", False):
|
|
||||||
patterns.append(re.compile(rf"{boundary_before}({value_rx}){boundary_after}", flags | re.MULTILINE))
|
|
||||||
prefixes = normalization.get("accepted_prefixes") or []
|
|
||||||
separators = normalization.get("prefix_value_separators") or [" "]
|
|
||||||
else:
|
|
||||||
prefixes = (rule.get("match") or {}).get("context_prefixes") or []
|
|
||||||
separators = (rule.get("match") or {}).get("context_separators") or [": ", ":"]
|
|
||||||
|
|
||||||
gap = r"\s*" if multiline else r"[ \t]*"
|
|
||||||
for prefix in prefixes:
|
|
||||||
prefix_rx = _literal_to_pattern(str(prefix), multiline)
|
|
||||||
for separator in separators:
|
|
||||||
separator_rx = _literal_to_pattern(str(separator), multiline)
|
|
||||||
patterns.append(
|
|
||||||
re.compile(
|
|
||||||
rf"{boundary_before}{prefix_rx}{separator_rx}{gap}({value_rx}){boundary_after}",
|
|
||||||
flags | re.MULTILINE,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": rule.get("id"),
|
|
||||||
"type": rule_type,
|
|
||||||
"kind": _placeholder_to_kind(rule.get("placeholder", "[MASK]")),
|
|
||||||
"placeholder": rule.get("placeholder", "[MASK]"),
|
|
||||||
"patterns": patterns,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def compile_active_admin_rules(data: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
compiled = {
|
|
||||||
"force_mask_terms": [],
|
|
||||||
"whitelist_phrases": [],
|
|
||||||
"detection_rules": [],
|
|
||||||
"active_rule_ids": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
for rule in data.get("rules", []) or []:
|
|
||||||
if not isinstance(rule, dict):
|
|
||||||
continue
|
|
||||||
if rule.get("status") != "active":
|
|
||||||
continue
|
|
||||||
compiled["active_rule_ids"].append(rule.get("id"))
|
|
||||||
rule_type = rule.get("type")
|
|
||||||
action = rule.get("action")
|
|
||||||
match = rule.get("match") or {}
|
|
||||||
|
|
||||||
if rule_type == "exact_term" and action == "mask":
|
|
||||||
value = str(match.get("exact_value", "")).strip()
|
|
||||||
if value:
|
|
||||||
compiled["force_mask_terms"].append(value)
|
|
||||||
elif rule_type == "preserve_phrase" and action == "preserve":
|
|
||||||
value = str(match.get("exact_value", "")).strip()
|
|
||||||
if value:
|
|
||||||
compiled["whitelist_phrases"].append(value)
|
|
||||||
elif rule_type in {"normalized_identifier", "contextual_identifier"} and action == "mask":
|
|
||||||
if _is_non_empty_string(match.get("canonical_value")):
|
|
||||||
compiled["detection_rules"].append(_compile_identifier_rule(rule))
|
|
||||||
|
|
||||||
compiled["force_mask_terms"] = _dedupe_keep_order(compiled["force_mask_terms"])
|
|
||||||
compiled["whitelist_phrases"] = _dedupe_keep_order(compiled["whitelist_phrases"])
|
|
||||||
return compiled
|
|
||||||
20
ano/pdf_natif/pseudonymise/FC14.audit.jsonl
Normal file
20
ano/pdf_natif/pseudonymise/FC14.audit.jsonl
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{"page": 0, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 0, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 0, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 0, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
348
ano/pdf_natif/pseudonymise/FC14.pseudonymise.txt
Normal file
348
ano/pdf_natif/pseudonymise/FC14.pseudonymise.txt
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
NNNN°°°° OOOOGGGGCCCC : ::: 11114444
|
||||||
|
FICHE MEDICALE DE RECUEIL DU PRATICIEN CONSEIL (une fiche par RUM)
|
||||||
|
Seul le recodage impactant la facturation est renseigné
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Dossier manquant : 0 Dates du séjour : 09/05/2023 au 10/05/2023
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
)sna(
|
||||||
|
egA
|
||||||
|
)sruoj(
|
||||||
|
egA exeS
|
||||||
|
.nred
|
||||||
|
ialéD selgèr egA
|
||||||
|
noitatseg
|
||||||
|
sdioP
|
||||||
|
eértne'd ed
|
||||||
|
eéruD ruojés edoM
|
||||||
|
eértne'd
|
||||||
|
ecnanevorP
|
||||||
|
edoM
|
||||||
|
eitros
|
||||||
|
ed
|
||||||
|
noitanitseD secnaés
|
||||||
|
bN
|
||||||
|
MUR
|
||||||
|
bN
|
||||||
|
HXE
|
||||||
|
j bN
|
||||||
|
BXE
|
||||||
|
epyT
|
||||||
|
BXE
|
||||||
|
j bN
|
||||||
|
Etablissement 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM Nature Nb
|
||||||
|
Lits dédiés SP UM IGS II Durée RUM
|
||||||
|
suppl. suppl.
|
||||||
|
0
|
||||||
|
N° RUM Etablissement : 1/2 0 29 C 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
0
|
||||||
|
N° RUM Recodage : 1/2 0 29 C 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
Codage de l’Etablissement Recodage
|
||||||
|
DP K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
Actes
|
||||||
|
Rappel : un code CIM de DAS suivi d’un astérisque correspond à une CMA exclue par le DP
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1 Accord
|
||||||
|
GHS injustifié : 0 SE FFM FSD Désaccord
|
||||||
|
En fonction des DP/DR et actes retenus par le PC, seul le recodage d'une des CMA les plus élevées ayant une incidence sur la racine GHM
|
||||||
|
ou sur la facturation des suppléments sera renseigné. Hors RSS injustifié avec actes externes, seuls les actes classants seront recodés
|
||||||
|
|
||||||
|
11114444
|
||||||
|
N° OGC :
|
||||||
|
FICHE MEDICALE DE RECUEIL DU PRATICIEN CONSEIL (une fiche par RUM)
|
||||||
|
Seul le recodage impactant la facturation est renseigné
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Dossier manquant : 0 Dates du séjour : 09/05/2023 au 10/05/2023
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
)sna(
|
||||||
|
egA
|
||||||
|
)sruoj(
|
||||||
|
egA exeS
|
||||||
|
.nred
|
||||||
|
ialéD selgèr egA
|
||||||
|
noitatseg
|
||||||
|
sdioP
|
||||||
|
eértne'd ed
|
||||||
|
eéruD ruojés edoM
|
||||||
|
eértne'd
|
||||||
|
ecnanevorP
|
||||||
|
edoM
|
||||||
|
eitros
|
||||||
|
ed
|
||||||
|
noitanitseD secnaés
|
||||||
|
bN
|
||||||
|
MUR
|
||||||
|
bN
|
||||||
|
HXE
|
||||||
|
j bN
|
||||||
|
BXE
|
||||||
|
epyT
|
||||||
|
BXE
|
||||||
|
j bN
|
||||||
|
Etablissement 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM Nature Nb
|
||||||
|
Lits dédiés SP UM IGS II Durée RUM
|
||||||
|
suppl. suppl.
|
||||||
|
1
|
||||||
|
N° RUM Etablissement : 2/2 0 53 C 0 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
1
|
||||||
|
N° RUM Recodage : 2/2 0 53 C 0 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
Codage de l’Etablissement Recodage
|
||||||
|
DP K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
HMFC004 1 CHOLÉCYSTECTOMIE COELIO. HMFC004 1
|
||||||
|
HMFC004 4 CHOLÉCYSTECTOMIE COELIO. HMFC004 4
|
||||||
|
Actes
|
||||||
|
Rappel : un code CIM de DAS suivi d’un astérisque correspond à une CMA exclue par le DP
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1 Accord
|
||||||
|
GHS injustifié : 0 SE FFM FSD Désaccord
|
||||||
|
En fonction des DP/DR et actes retenus par le PC, seul le recodage d'une des CMA les plus élevées ayant une incidence sur la racine GHM
|
||||||
|
ou sur la facturation des suppléments sera renseigné. Hors RSS injustifié avec actes externes, seuls les actes classants seront recodés
|
||||||
|
|
||||||
|
FICHE MEDICALE DE CONCERTATION
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document couvert par le secret médical
|
||||||
|
Ne peut pas être produit aux services administratifs de l’établissement et des organismes de sécurité sociale
|
||||||
|
Nom du praticien-conseil : V VAILLENDET Nom du médecin du DIM :
|
||||||
|
Homme de 61 ans
|
||||||
|
Antécédent :
|
||||||
|
Pancréatite aiguë d'origine indéterminée d'évolution favorable.
|
||||||
|
Hospitalisation du 9 au 10/5/23
|
||||||
|
Admis à distance de l’épisode de pancréatite pour une
|
||||||
|
cholécystectomie par laparoscopie
|
||||||
|
En peropératoire présence de calculs intra-vésiculaires => en
|
||||||
|
faveur d'une origine lithiasique de cette PA.
|
||||||
|
La cholangiographie peropératoire ne retrouvait pas de calcul
|
||||||
|
dans la VBP.
|
||||||
|
Codage DP :
|
||||||
|
Cholécystectomie « à froid » suite à une pancréatite
|
||||||
|
Le CRO mentionne une légère inflammation séquellaire de la
|
||||||
|
pancréatite.
|
||||||
|
Il n’y a donc pas de pancréatite aigüe sur ce séjour, c’est un
|
||||||
|
antécédent
|
||||||
|
Pas de notion de cholécystite aigue
|
||||||
|
Codage retenu : K80.1 « Calcul de la vésicule biliaire avec une
|
||||||
|
autre forme de cholécystite »
|
||||||
|
Nb : 2 RUM
|
||||||
|
Probable changement d’unité après chirurgie => même codage
|
||||||
|
pour els 2 RUM
|
||||||
|
Date de concertation :
|
||||||
|
NOM et SIGNATURE du MEDECIN RESPONSABLE du CONTRÔLE NOM et SIGNATURE du MEDECIN du DIM
|
||||||
|
Dr Gilles DE MONREDON Atteste avoir pris connaissance des éléments du dossier y compris
|
||||||
|
ceux couverts par le secret médical et des arguments soutenus par
|
||||||
|
les médecins contrôleurs et avoir eu l’opportunité d’en débattre
|
||||||
|
contradictoirement
|
||||||
|
NOM du ou des autres participants à la concertation
|
||||||
|
NOM du ou des autres membres de l’équipe de contrôle ayant
|
||||||
|
participé à la concertation
|
||||||
|
|
||||||
|
FICHE ADMINISTRATIVE DE CONCERTATION 1/2
|
||||||
|
(à établir lors de la concertation avec le médecin du DIM)
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document susceptible d’être produit aux services administratifs de l’établissement et des organismes de sécurité sociale,
|
||||||
|
n’inscrire aucun élément couvert par le secret médical.
|
||||||
|
ARGUMENTAIRE DU MEDECIN CONTROLEUR
|
||||||
|
142 : La facturation du GHS par l’établissement n’est pas conforme à l’article 1 de l’arrêté du 19 février 2015 modifié du fait d’un non-
|
||||||
|
respect des règles de codage édictées dans l’annexe II de l’arrêté du 23 décembre 2016 modifié. En préalable, chapitre VI, paragraphe
|
||||||
|
1.2 : « Les circonstances du diagnostic préalable n’importent pas (…) La situation de traitement est présente lorsque le diagnostic de
|
||||||
|
l’affection est fait au moment de l’entrée du patient dans l’unité médicale et que l’admission a pour but le traitement de l’affection. »
|
||||||
|
Le non-respect des règles porte sur le diagnostic principal (DP) codé par l’établissement dans le résumé d’unité médicale (RUM). Le DP
|
||||||
|
n’est pas conforme aux règles de codage des diagnostics rappelées par l’annexe II, chapitre VI, paragraphe 1.2.2.1 : « Dans la situation
|
||||||
|
de traitement unique chirurgical, le DP est en général la maladie opérée [Règle T3]. (…) Le diagnostic résultant de l’intervention peut
|
||||||
|
être différent du diagnostic préopératoire (…). Le DP doit en effet être énoncé en connaissance de l’ensemble des informations
|
||||||
|
acquises au cours du séjour. » Au vu des éléments présents dans le dossier du patient, alors que l’admission a été motivée par le
|
||||||
|
traitement chirurgical d’une affection, l’établissement n’a pas retenu le code de cette affection en DP.
|
||||||
|
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Données du
|
||||||
|
séjour : )sna(
|
||||||
|
egA )sruoj(
|
||||||
|
egA exeS .nred
|
||||||
|
selgèr
|
||||||
|
ialéD noitatseg
|
||||||
|
egA eértne'd
|
||||||
|
sdioP ed
|
||||||
|
eéruD ruojés eértne'd
|
||||||
|
edoM ecnanevorP eitros
|
||||||
|
edoM
|
||||||
|
ed noitanitseD secnaés
|
||||||
|
bN MUR
|
||||||
|
bN HXE
|
||||||
|
j
|
||||||
|
bN BXE
|
||||||
|
epyT BXE
|
||||||
|
j
|
||||||
|
bN
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
Etablissement : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM : Lits dédiés SP UM IGS II Durée RUM Nature
|
||||||
|
suppl. Nb
|
||||||
|
suppl.
|
||||||
|
N° RUM Etablissement : 1/2 0 29 C 0 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
N° RUM Recodage : 1/2 0 29 C 0 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
Codage de l’Etablissement : Recodage
|
||||||
|
DP : K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Actes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil : Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1
|
||||||
|
GHS injustifié : 0 SE FFM FSD
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Données du
|
||||||
|
séjour : )sna(
|
||||||
|
egA )sruoj(
|
||||||
|
egA exeS .nred
|
||||||
|
selgèr
|
||||||
|
ialéD noitatseg
|
||||||
|
egA eértne'd
|
||||||
|
sdioP ed
|
||||||
|
eéruD ruojés eértne'd
|
||||||
|
edoM ecnanevorP eitros
|
||||||
|
edoM
|
||||||
|
ed noitanitseD secnaés
|
||||||
|
bN MUR
|
||||||
|
bN HXE
|
||||||
|
j
|
||||||
|
bN BXE
|
||||||
|
epyT BXE
|
||||||
|
j
|
||||||
|
bN
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
Etablissement : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM : Lits dédiés SP UM IGS II Durée RUM Nature
|
||||||
|
suppl. Nb
|
||||||
|
suppl.
|
||||||
|
N° RUM Etablissement : 2/2 0 53 C 0 1 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
N° RUM Recodage : 2/2 0 53 C 0 1 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
Codage de l’Etablissement : Recodage
|
||||||
|
DP : K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Actes : HMFC004 1 CHOLÉCYSTECTOMIE COELIO. HMFC004 1
|
||||||
|
HMFC004 : 4 CHOLÉCYSTECTOMIE COELIO. HMFC004 4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil : Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1
|
||||||
|
GHS injustifié : 0 SE FFM FSD
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document couvert par le secret médical
|
||||||
|
Ne peut pas être produit aux services administratifs de l’établissement et des organismes de sécurité sociale
|
||||||
|
Nom du praticien-conseil : V VAILLENDET Nom du médecin du DIM :
|
||||||
|
Homme de 61 ans
|
||||||
|
Antécédent : Pancréatite aiguë d'origine indéterminée d'évolution favorable.
|
||||||
|
Hospitalisation du 9 au 10/5/23
|
||||||
|
Admis à distance de l’épisode de pancréatite pour une
|
||||||
|
cholécystectomie par laparoscopie
|
||||||
|
En peropératoire présence de calculs intra-vésiculaires => en
|
||||||
|
faveur d'une origine lithiasique de cette PA.
|
||||||
|
La cholangiographie peropératoire ne retrouvait pas de calcul
|
||||||
|
dans la VBP.
|
||||||
|
Codage DP :
|
||||||
|
Cholécystectomie « à froid » suite à une pancréatite
|
||||||
|
Le CRO mentionne une légère inflammation séquellaire de la
|
||||||
|
pancréatite.
|
||||||
|
Il n’y a donc pas de pancréatite aigüe sur ce séjour, c’est un
|
||||||
|
antécédent
|
||||||
|
Pas de notion de cholécystite aigue
|
||||||
|
Codage retenu : K80.1 « Calcul de la vésicule biliaire avec une
|
||||||
|
autre forme de cholécystite »
|
||||||
|
Nb : 2 RUM
|
||||||
|
Probable changement d’unité après chirurgie => même codage
|
||||||
|
pour els 2 RUM
|
||||||
|
NOM et SIGNATURE du MEDECIN RESPONSABLE du CONTRÔLE
|
||||||
|
Dr Gilles DE MONREDON
|
||||||
|
NOM du ou des autres membres de l’équipe de contrôle ayant
|
||||||
|
participé à la concertation : NOM et SIGNATURE du MEDECIN du DIM
|
||||||
|
Atteste avoir pris connaissance des éléments du dossier y compris
|
||||||
|
ceux couverts par le secret médical et des arguments soutenus par
|
||||||
|
les médecins contrôleurs et avoir eu l’opportunité d’en débattre
|
||||||
|
contradictoirement
|
||||||
|
NOM du ou des autres participants à la concertation
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document susceptible d’être produit aux services administratifs de l’établissement et des organismes de sécurité sociale,
|
||||||
|
n’inscrire aucun élément couvert par le secret médical.
|
||||||
|
ARGUMENTAIRE DU MEDECIN CONTROLEUR
|
||||||
|
142 : La facturation du GHS par l’établissement n’est pas conforme à l’article 1 de l’arrêté du 19 février 2015 modifié du fait d’un non-
|
||||||
|
respect des règles de codage édictées dans l’annexe II de l’arrêté du 23 décembre 2016 modifié. En préalable, chapitre VI, paragraphe
|
||||||
|
1.2 : « Les circonstances du diagnostic préalable n’importent pas (…) La situation de traitement est présente lorsque le diagnostic de
|
||||||
|
l’affection est fait au moment de l’entrée du patient dans l’unité médicale et que l’admission a pour but le traitement de l’affection. »
|
||||||
|
Le non-respect des règles porte sur le diagnostic principal (DP) codé par l’établissement dans le résumé d’unité médicale (RUM). Le DP
|
||||||
|
n’est pas conforme aux règles de codage des diagnostics rappelées par l’annexe II, chapitre VI, paragraphe 1.2.2.1 : « Dans la situation
|
||||||
|
de traitement unique chirurgical, le DP est en général la maladie opérée [Règle T3]. (…) Le diagnostic résultant de l’intervention peut
|
||||||
|
être différent du diagnostic préopératoire (…). Le DP doit en effet être énoncé en connaissance de l’ensemble des informations
|
||||||
|
acquises au cours du séjour. » Au vu des éléments présents dans le dossier du patient, alors que l’admission a été motivée par le
|
||||||
|
traitement chirurgical d’une affection, l’établissement n’a pas retenu le code de cette affection en DP.
|
||||||
|
|
||||||
|
|
||||||
|
| ||||||