Décisions d'architecture (ADR)
Fiches courtes : Contexte → Décision → Conséquences → Source. Une décision majeure par fiche. Index :
| # | Décision | Statut |
|---|---|---|
| ADR-001 | Résoudre vers Mondo (CC0) comme pivot | adopté |
| ADR-002 | Contract-first (OpenAPI source de vérité) | adopté |
| ADR-003 | Snapshot dénormalisé > FK (audit) | adopté |
| ADR-004 | Codec JSONB asyncpg | adopté |
| ADR-005 | Identifiant vs contenu (licence) | adopté |
| ADR-006 | Émission par précédence + abstention | adopté |
| ADR-007 | Voie 1 (CC0) vs voie 1.5 (UMLS licenciée) | adopté |
| ADR-008 | Repli NCIt CC0 | adopté |
| ADR-009 | Curation onco en 2 temps | adopté |
| ADR-010 | Exécuteur de build = runner host | adopté |
| ADR-011 | Activation in-place (bind mount) | adopté |
| ADR-012 | meta.json déclaré + backfill | adopté |
| ADR-013 | Pseudonymisation à l’export | adopté |
| ADR-014 | Build Mondo manuel | adopté |
| ADR-015 | SPHN : SNOMED, identifiants seuls ; TNM/BBMRI hors périmètre | adopté |
| ADR-016 | Exigence de granularité SCTID | ouvert |
Résoudre vers Mondo (CC0) comme pivot.
- Contexte : il faut une ontologie maladie riche, multilingue-friendly et redistribuable.
- Décision : Mondo comme pivot (CC0, riche en xrefs) ; SNOMED atteint via un pont.
- Conséquences : open-core possible ; mais trou-pivot (granularité) → ADR-006/016.
- Source : notes/15, 33, 44 → Pont terminologique & émission (Mondo→SNOMED).
Contract-first (OpenAPI = source de vérité).
- Contexte : 2 langages (Python API, TS frontend) doivent rester synchrones.
- Décision :
openapi.yamlest la vérité ; types TS + schémas Pydantic en dérivent. - Conséquences : drift cassé au compile-time TS ; éditer le contrat d’abord.
- Source : repo/CLAUDE.md → Architecture.
Snapshot dénormalisé > FK pour l’audit.
- Contexte : l’audit doit survivre aux changements (renommage user, suppression).
- Décision : figer des libellés au moment de l’action (
validated_by,actor_label) plutôt que des FK. - Conséquences : audit stable et lisible ; léger surcoût de stockage.
- Source : repo/CLAUDE.md.
Codec JSONB asyncpg (encoder=json.dumps).
- Contexte : champs JSONB (settings, candidates, snapshots).
- Décision : codec posé → passer les dict directement, PAS de
json.dumps()+ cast::jsonb(sinon double-encodage, JSONB stocké comme string). - Conséquences : au décodage, certains JSONB reviennent en
str→json.loadsdéfensif. - Source : repo/CLAUDE.md.
Identifiant vs contenu (licence).
- Contexte : SNOMED/UMLS licenciés ; Mondo/NCIt CC0.
- Décision : ne redistribuer que des identifiants ; jamais le contenu licencié ; libellés SNOMED lus à la volée, jamais persistés ; artefacts licenciés BYO RO.
- Conséquences : snapshots label-free ; dashboard = comptes ; séparation physique.
- Source : notes/50, D22 → Modèle de licence (open-core).
Émission par précédence + abstention.
- Contexte : émission aveugle = 33–39 % de faux ; justesse plafonnée par le pivot.
- Décision : paliers
exact > confirmed > candidate > approximate > ncit > uncoded; auto-émission seulement sur ancre confiante ; sinon dégradation / abstention. - Conséquences : moins de codes mais fiables ; charge de confirmation humaine.
- Source : notes/47-49 → Pont terminologique & émission (Mondo→SNOMED).
Voie 1 (CC0) vs voie 1.5 (UMLS licenciée).
- Contexte : SNOMED atteignable directement (xref Mondo CC0) ou via UMLS (licencié).
- Décision : séparer par tier ; voie 1.5 produit des codes
candidatePENDING (cardinalité = signal de confiance), confirmés par un humain. - Conséquences : carte UMLS BYO gitignorée ; provenance par code.
- Source : notes/50.
Repli NCIt (CC0) quand SNOMED absent.
- Contexte : certains concepts n’ont pas de SNOMED émis.
- Décision : émettre un code NCIt (CC0) plutôt que rien — code d’interop alternatif.
- Conséquences : meilleure couverture sans code SNOMED douteux.
- Source : notes/historique émission → Pont terminologique & émission (Mondo→SNOMED).
Curation onco en 2 temps (pending_oncology).
- Contexte : une tumeur = ancre + axes ; valider l’ancre ne suffit pas.
- Décision : 1ᵉʳ accept →
pending_oncology(file dédiée) ; 2ᵉ accept (axes) →validated. - Conséquences : file onco séparée ; tests doivent éviter les mentions onco si non voulu.
- Source : notes/41 → Moteur compositionnel & oncologie (MC).
Exécuteur de build = runner host.
- Contexte : l’API monte l’index RO et n’embarque pas le builder (kb_builders ⊂ MC, host).
- Décision : l’UI enfile une requête ; un runner host (timer systemd) la consomme. Pas de conteneur builder dédié.
- Conséquences : daemon out-of-band + accès DB host ; SNOMED vérifié réel ; UMLS = +restart.
- Source : notes/54 → Gestion des ontologies, Opérations & infra.
Activation in-place (os.replace), car l’index est un bind mount.
- Contexte : un swap par rename de dossier n’est pas vu par le conteneur (bind mount).
- Décision : build dans un dossier temp →
os.replacefichier par fichier (atomique, même FS) ; pas de restart conteneur. - Conséquences : activation sans downtime ; brève fenêtre d’incohérence inter-fichiers acceptable (op admin).
- Source : notes/54.
meta.json déclaré + backfill (vs scan live).
- Contexte : le dashboard dérivait/scannait (couverture, version) à chaque lecture.
- Décision : les builders émettent les champs déclarés ; un backfill les pose sans reprocess ; le dashboard lit le déclaré avec fallback.
- Conséquences : plus rapide, traçable (provenance/sha) ; rétrocompatible.
- Source : notes/52 → Gestion des ontologies.
Pseudonymisation déterministe à l’export.
- Contexte : les livrables peuvent quitter l’institution (nLPD).
- Décision :
curator_<sha256(user_id)[:8]>à la sortie (pas de colonne en base). - Conséquences : 0 PII dans les livrables biobanque/SPHN ;
audit_trainingreste nominatif (interne). - Source : Jalon 6 → Imports & exports (interop).
Build Mondo manuel (KB linker = xmen GPU).
- Contexte : le KB du linker est un index xmen/FAISS (rebuild GPU + alias DE séparés), pas l’index compositionnel MC.
- Décision : Mondo détecté dans l’UI mais non déclenchable (un one-click régresserait le multilingue / serait lourd) ; build = procédure host documentée.
- Conséquences : asymétrie assumée avec SNOMED/UMLS (triggerables) ; Phase 3 si besoin.
- Source : notes/54.
SPHN : SNOMED, identifiants seuls ; TNM/BBMRI hors périmètre.
- Contexte : SPHN attend historiquement de l’ICD-O-3 ; Messier produit du SNOMED.
- Décision : émettre du SNOMED (seule option légale), identifiants seuls ; TNM (staging) et specimen BBMRI hors périmètre (linking ≠ staging).
- Conséquences : binding site/histologie ICD-O-3-vs-SNOMED = décision externe SPHN / CHU partenaire.
- Source : notes/51, 53 → Imports & exports (interop).
Exigence de granularité SCTID (ouvert).
- Contexte : le pivot Mondo plafonne la justesse exacte ; le pilote doit choisir.
- Décision (à prendre) : SCTID niveau maladie (granularité Mondo, atteignable) vs SCTID clinique exact (nécessite un linker SNOMED-direct, piste O3).
- Conséquences : conditionne l’ouverture de O3 (56 % des maladies rencontrées sans pivot).
- Source : notes/48 → Pont terminologique & émission (Mondo→SNOMED).