Il s'agit du troisième et dernier billet de la série sur la fusion dans MDMbox. Le premier a présenté notre conception pilotée par le client pour l'opération $merge : le client envoie un Bundle de transaction décrivant chaque PUT, POST et DELETE ; le serveur l'encapsule avec Task + Provenance et l'exécute de manière atomique. Le deuxième a expliqué pourquoi la fusion ne peut pas reposer sur un seul algorithme : il existe au moins quatre axes de politique indépendants, chacun avec plusieurs variantes déployées parmi les fournisseurs MPI, les plateformes MDM, les DSE et les registres nationaux. Et c'est ici que nous parlerons enfin de l'opération inverse, $unmerge.
La spécification FHIR nous donne Patient/$merge au niveau de maturité 0 et aucun $unmerge du tout, pourtant chaque MPI et MDM en production doit prendre en charge l'inversion, car tout Index de patients maîtres finit par fusionner deux dossiers qui n'auraient pas dû l'être. La question n'est pas de savoir si une plateforme MDM a besoin de la défusion, mais comment celle-ci doit se comporter.
Pourquoi $unmerge n'est pas simplement $merge à l'envers
La tentation est de traiter la défusion comme « rejouer la transaction de fusion à rebours ». Lire la Provenance, récupérer les versions antérieures à la fusion depuis l'historique, les réécrire. Terminé.
Cela fonctionne pour la moitié mécanique, mais pas pour la moitié politique du problème. Voyons quelques exemples.
Écart temporel. Anna Schmidt est inscrite deux fois sous Patient/anna-old et Patient/anna-new. Le lundi, Patient/anna-old est fusionné dans Patient/anna-new, enregistré sous Task/merge-anna avec une Provenance associée. Le mardi, trois nouveaux Encounters sont publiés dans Patient/anna-new (Encounter/visit-tue-1..3), des visites enregistrées par la clinique après l'unification de l'identité. Le mercredi, une défusion est demandée parce que la fusion initiale était erronée. Ces trois Encounters n'ont pas de version antérieure à la fusion. Ils n'ont jamais existé dans Patient/anna-old. Où vont-ils maintenant : retour vers la source restaurée, conservés sur la cible, distribués selon une règle clinique, ou acheminés vers une file d'attente de gestionnaire?
Il n'existe pas d'algorithme unique et définitif pour cela. Chaque dossier d'Encounter peut appartenir à l'un ou l'autre patient selon lequel a réellement effectué la visite, et le serveur ne le sait pas.
Suppression définitive. Si le patient source a été supprimé définitivement au moment de la fusion (le modèle VistA décrit dans notre billet précédent), le dossier source lui-même a disparu des tables actives. L'API History conserve toujours ses versions antérieures, mais recréer une ressource active à partir d'une entrée supprimée est une décision de politique : faut-il créer un nouvel identifiant et conserver la correspondance dans la piste d'audit de la défusion, restaurer l'identifiant original en acceptant l'écart, ou refuser la défusion entièrement? Différentes implémentations MDM répondent à cela différemment pour des raisons sans lien avec FHIR : règles de conservation, abonnés en aval, exigences d'audit jurisdictionnelles.
Cette frontière détermine l'interface : $unmerge est un plan de restauration fourni par le client, et non une commande d'annulation côté serveur.
Le client envoie la Task de fusion originale et un Bundle de transaction décrivant l'état restauré souhaité. Le serveur valide ce plan, vérifie les verrous optimistes, l'applique de manière atomique et écrit la piste d'audit Task/Provenance.
L'opération $unmerge est donc symétrique à $merge de notre premier billet.
POST $unmerge
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{"name": "task", "valueReference": {"reference": "Task/merge-anna"}},
{"name": "preview", "valueBoolean": false},
{
"name": "plan",
"resource": {
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"resource": {"resourceType": "Patient", "id": "anna-old", "...": "restored from history"},
"request": {"method": "PUT", "url": "Patient/anna-old", "ifMatch": "W/\"3\""}
},
{
"resource": {"resourceType": "Patient", "id": "anna-new", "...": "rolled back to pre-merge state"},
"request": {"method": "PUT", "url": "Patient/anna-new", "ifMatch": "W/\"7\""}
},
{
"resource": {"resourceType": "Encounter", "id": "visit-jan",
"subject": {"reference": "Patient/anna-old"}},
"request": {"method": "PUT", "url": "Encounter/visit-jan", "ifMatch": "W/\"4\""}
},
{
"resource": {"resourceType": "Encounter", "id": "visit-tue-1",
"subject": {"reference": "Patient/anna-new"}},
"request": {"method": "PUT", "url": "Encounter/visit-tue-1", "ifMatch": "W/\"1\""}
}
]
}
}
]
}
La seule entrée obligatoire est une référence à la Task de fusion originale. Tout le reste (quelle version de quelle ressource est restaurée où, ce qui arrive aux données ajoutées après la fusion, que faire de la source si elle a été supprimée) se trouve dans le Bundle plan.
preview: true effectue un essai à blanc du même pipeline de validation. Le serveur retourne l'OperationOutcome qu'il aurait retourné ainsi que la Provenance résultante, de sorte que le client peut montrer à un gestionnaire exactement ce qu'une défusion touchera avant de l'autoriser.
Les en-têtes ifMatch assurent le verrouillage optimiste, tout comme dans la fusion. Si une ressource a changé entre le moment où le client a construit le plan de défusion et le moment où le serveur l'a exécuté, la transaction échoue. Nous n'écrasons pas silencieusement le travail que le gestionnaire n'a pas vu.
Construire le plan à partir de la Provenance
Le client n'a pas à construire le plan de toutes pièces. La Provenance de la fusion contient déjà les données dont le client a besoin.
Pour chaque ressource que la fusion a touchée, la Provenance de la fusion a enregistré une référence versionnée sous entity[].what, par exemple Patient/anna-old/_history/2. Il s'agit de l'état antérieur à la fusion. L'API History le résout directement :
GET Patient/anna-old/_history/2
Un plan par défaut naïf est alors une transformation en une seule passe :
- Lire la Task de fusion, suivre ses listes
Provenance.targetetProvenance.entity. - Pour chaque ressource concernée, récupérer sa version antérieure à la fusion depuis l'historique.
- Émettre une entrée
PUTqui restaure cette version, avecifMatchcorrespondant à la version actuelle de la ressource (afin que les modifications concurrentes soient détectées). - Pour les ressources créées lors de la fusion qui n'avaient pas d'état antérieur (typiquement : un dossier Linkage ou une assertion de correspondance), émettre un
DELETE. Les valeurs copiées dans une ressource existante (par exemple, des identifiants ajoutés au Patient cible) disparaissent par lePUTde restauration de cette ressource.
La majeure partie de l'interface Steward de MDMbox utilise exactement cette valeur par défaut. Le gestionnaire voit le Bundle inverse proposé, modifie les entrées qui nécessitent des décisions de politique, et soumet.
Ce que la valeur par défaut ne gère pas, c'est l'écart temporel. Les ressources créées ou modifiées après l'horodatage de la fusion ne figurent pas dans la Provenance de la fusion. Elles constituent un nouvel état net, et les créneaux de politique ci-dessous permettent au gestionnaire d'indiquer quoi en faire.
La chaîne d'audit
La défusion n'efface pas la fusion — elle s'y ajoute. Après la validation de $unmerge, trois artefacts coexistent pour Task/merge-anna :
- La Task de fusion originale, avec
businessStatusbasculé demergedàunmergeddans la même transaction. LeifMatchsur ce basculement détecte les tentatives de défusion simultanées : avec deux appels$unmergeparallèles pour la même Task de fusion, l'un réussit et l'autre obtient un conflit de version. - La Provenance de la fusion, intacte. Elle nomme toujours les versions antérieures à la fusion dans
entity[].what. La défusion n'invalide pas la piste d'audit de la fusion; elle s'y greffe. - Une nouvelle Task de défusion avec
basedOn → Task/merge-anna, associée à une Provenance de défusion pointant vers les versions antérieures à la défusion de tout ce que le plan inverse a touché.
La chaîne se lit comme une requête directe :
GET /Task/merge-anna # the merge, businessStatus=unmerged
GET /Task?code=unmerge&based-on=Task/merge-anna # the unmerge that reversed it
GET /Provenance?target=Task/unmerge-anna # the unmerge's own audit trail
GET /Patient/anna-old/_history # full timeline of the source
Ce dernier appel est celui que les auditeurs et les gestionnaires exécutent réellement lorsque la source est restaurée sous son identifiant original. Patient/anna-old/_history se lit comme un récit continu : créé, actif, supprimé lors de la fusion, recréé lors de la défusion, actif à nouveau. Chaque transition est associée à une Provenance, chaque Provenance cite une Task, chaque Task de défusion cite la Task de fusion qu'elle a inversée. Si un déploiement crée un nouvel identifiant à la place, la continuité est explicite dans la Provenance de la défusion plutôt que dans un seul historique de Patient.
La même structure s'applique de manière récursive. Si Patient/anna-old est à nouveau fusionné le mois prochain puis à nouveau défusionné, la chronologie gagne un autre cycle fusion–défusion ancré à sa propre paire de Tasks. Rien ne s'effondre, rien n'est réécrit — l'API History est le journal d'audit déroulé.
Quatre créneaux de politique : les axes de fusion inversés
Un créneau de politique est l'endroit dans le plan de défusion où le client doit prendre une décision explicite. Certains créneaux modifient le Bundle de transaction lui-même. D'autres créent du travail de suivi pour les gestionnaires, les correspondances ou les systèmes en aval. Ensemble, ils rendent l'état restauré intentionnel plutôt que simplement réversible historiquement.
Survivance → réassignation des champs. La fusion demandait quelle valeur l'emporte quand deux dossiers sont en conflit. La défusion demande : pour les champs écrits après la fusion, quel dossier restauré les reçoit? Un nouveau numéro de téléphone enregistré le mardi : va-t-il sur la source, la cible, ou les deux? MDMbox traite cela comme une décision client par champ, avec une valeur par défaut sécuritaire de « rester sur la cible » (le dossier qui était actif au moment de l'écriture du champ) et un indicateur dans la Provenance pour qu'un gestionnaire puisse y revenir.
Gestion des références → réassignation des références. La fusion demandait quoi faire des pointeurs vers la source. La défusion demande : vers où pointent maintenant les nouvelles références, celles créées après la fusion qui ont toujours pointé vers la cible? Une réclamation déposée le mardi référençant Patient/anna-new aura maintenant deux patients dans sa portée, et la spécification n'offre aucune orientation. Nous en faisons une décision explicite dans le plan par type de référence. La valeur par défaut est de conserver les nouvelles références sur la cible à moins que le client ne remplace.
Disposition de la source → restauration de la source. La fusion demandait ce qu'il advient de la source. La défusion demande : qu'est-ce qui revient? Si la source a été conservée inactive avec replaced-by, la restauration est mécanique : basculer active à vrai, supprimer le lien. Si la source a été supprimée définitivement, le client doit choisir : restaurer l'identifiant original depuis l'historique (en acceptant l'écart de version), créer un nouvel identifiant avec une Provenance de défusion qui fait correspondre l'ancienne source versionnée au nouvel identifiant de Patient, ou refuser la défusion.
Effets en aval → file de réévaluation. La fusion indiquait que les calculs cumulatifs et les graphes de liens nécessitent une réévaluation. La défusion dit la même chose, avec une nuance supplémentaire : tout ce qui a été construit durant la période fusionnée (dose de rayonnement recalculée, appartenance à une cohorte recalculée, caches de tiers, le propre graphe de liens probabilistes du MPI) ne peut plus être considéré comme fiable sans révision pour l'un ou l'autre des patients restaurés. La Task de défusion et sa Provenance constituent le point d'ancrage d'abonnement : les consommateurs en aval (interface Steward, recalculateurs de dose cumulative, le correspondant) surveillent Task?code=unmerge et réévaluent les ressources listées dans Provenance.target. Le serveur publie l'événement d'audit; ce qu'il faut recalculer est une décision en aval.
Le modèle est le même que dans $merge. Le serveur ne dispose pas du contexte clinique pour prendre ces décisions. C'est le côté client qui doit les prendre.
Après la défusion : Refusion et rétroaction au correspondant
Une fois que $unmerge est validé, le businessStatus de la Task de fusion affiche unmerged. Le garde de pré-fusion de MDMbox (la requête qui empêche la fusion d'une source déjà fusionnée) recherche Task?code=merge&business-status=merged&subject={ref} et ne trouve plus l'ancienne Task. Patient/anna-old est admissible à une nouvelle fusion. C'est intentionnel : la défusion inverse entièrement le cycle de vie de la fusion, y compris son blocage sur la refusion.
Si un être humain vient de décider que deux patients sont différents, le correspondant ne devrait pas les refusionner silencieusement lors de la prochaine exécution par lot. Le raisonnement contre l'automatisation est fondé : une défusion peut annuler une erreur technique (mauvais identifiants dans le plan de fusion, une source pointant vers le mauvais dossier) plutôt qu'affirmer une distinction clinique (ces personnes sont vraiment différentes). Le serveur ne peut pas faire la différence.
MDMbox garde les deux signaux séparés. La Task de défusion et la Provenance constituent le dossier public de l'inversion, interrogeable par le correspondant, l'interface Steward ou tout abonné en aval. Une assertion de non-fusion, lorsqu'un déploiement MDM en veut une, relève de la responsabilité du client pour l'écrire, et puisque le plan de défusion est un Bundle de transaction standard, le client insère un POST pour cette assertion aux côtés des entrées du plan inverse.
Le support FHIR naturel est une ressource Linkage pointant vers les deux patients restaurés, avec le code « do-not-merge » du déploiement dans une extension (les codes Linkage.item.type intégrés comme source/alternate/historical ne couvrent pas les assertions de correspondance) :
{
"resource": {
"resourceType": "Linkage",
"active": true,
"item": [
{"type": "alternate", "resource": {"reference": "Patient/anna-old"}},
{"type": "alternate", "resource": {"reference": "Patient/anna-new"}}
],
"extension": [{
"url": "http://mdmbox.dev/fhir/StructureDefinition/linkage-assertion",
"valueCode": "do-not-merge"
}]
},
"request": {"method": "POST", "url": "Linkage"}
}
Une alternative en place est Patient.link avec type=seealso et la même extension sur le lien : moins de ressources, mais l'état du correspondant se retrouve mêlé aux données démographiques dans la chaîne de versions du Patient. La plupart des déploiements préféreront le Linkage autonome afin que les règles « ne pas fusionner » du correspondant vivent dans leur propre ressource et puissent être interrogées, expirées ou révoquées sans toucher aux versions du Patient.
Cela a la même atomicité que tout le reste : la défusion et l'assertion sont validées ensemble, ou aucune ne l'est. Le contrat du serveur s'arrête à la frontière du Bundle; la forme que prend l'assertion et les dossiers qu'elle couvre relèvent du choix du client.
Le même principe s'applique aux chaînes de fusion. Une cible peut avoir été fusionnée plus d'une fois (A → B, puis C → B); le serveur ne l'empêche pas. Lorsque le client défusionne l'une d'elles par la suite, le plan inverse doit tenir compte de l'état actuel de B qui reflète les deux fusions, pas seulement celle en cours d'inversion. Chaque fusion possède sa propre Provenance, donc les données sont là, mais les appliquer correctement à travers des fusions qui se chevauchent est une décision du client, pas du serveur.
Certaines défusions nécessitent également un point de contrôle de politique avant que le plan inverse ne soit autorisé à être validé. Le cas canonique est l'état clinique cumulatif : dose de rayonnement à vie, totaux cumulatifs d'opioïdes, ou tout calcul signé qui serait invalidé par la redistribution d'événements entre deux patients. Le contrat portable n'est pas « toujours refuser » ou « toujours procéder ». La Task de fusion est le point d'attache pour ce verrou; l'aperçu peut le signaler comme un OperationOutcome; le déploiement décide si la défusion est bloquée en ligne, acheminée vers une file de révision humaine, ou autorisée pendant que les totaux dérivés restent en quarantaine.
Clore la série
Ce billet complète la trilogie sur la fusion. Le premier a défini un $merge piloté par le client et a plaidé pour déplacer la politique hors du serveur. Le deuxième a montré que la politique n'est pas une seule décision mais quatre, et que chaque fournisseur MPI en production livre une combinaison différente. Celui-ci reproduit le contrat pour la direction inverse : même interface, même quatre axes inversés en questions de défusion, même division du travail entre les invariants du serveur et la politique du client.
La mise en œuvre complète de $merge / $unmerge est disponible dès aujourd'hui dans MDMbox, une plateforme d'Index de patients maîtres et MDM native FHIR.




