---
{
  "title": "Conception de $unmerge : Inverser la fusion de patients FHIR",
  "description": "FHIR ne dispose d'aucune opération $unmerge. Nous en avons construit une qui inverse une fusion à l'aide de l'API Provenance + History et laisse le client décider quoi faire des données ajoutées entre la fusion et la défusion.",
  "date": "2026-05-22",
  "author": "Ivan Shukshin",
  "reading-time": "15 min read",
  "tags": ["FHIR Standard", "Data Quality", "Analytics", "Master Data Management", "Integrations", "MDMbox"],
  "tldr": "$unmerge reflète l'interface de $merge : le serveur est responsable des invariants (atomicité, audit, cycle de vie), le client est responsable de la politique. La partie facile consiste à restaurer l'état antérieur à la fusion : Task, Provenance et l'API History nous fournissent tout ce qu'il faut pour construire un Bundle de transaction inverse. La partie difficile est l'écart temporel : les données ajoutées entre la fusion et la défusion.",
  "utm-campaign": "fhir_expert",
  "utm-content": "unmerge-design"
}
---

Il s'agit du troisième et dernier billet de la série sur la fusion dans MDMbox. Le [premier](/blog/beyond-patient-merge) 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](/blog/mpi-vendors-patient-merge) 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?

![The temporal gap: data added between merge and unmerge has no pre-merge version to restore to](temporal-gap.svg)

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](/blog/mpi-vendors-patient-merge)), 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](/blog/beyond-patient-merge).

```http
POST $unmerge
Content-Type: application/fhir+json
```

```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 :

```http
GET Patient/anna-old/_history/2
```

Un plan par défaut naïf est alors une transformation en une seule passe :

1. Lire la Task de fusion, suivre ses listes `Provenance.target` et `Provenance.entity`.
2. Pour chaque ressource concernée, récupérer sa version antérieure à la fusion depuis l'historique.
3. Émettre une entrée `PUT` qui restaure cette version, avec `ifMatch` correspondant à la version *actuelle* de la ressource (afin que les modifications concurrentes soient détectées).
4. 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 le `PUT` de 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` :

1. **La Task de fusion originale**, avec `businessStatus` basculé de `merged` à `unmerged` dans la même transaction. Le `ifMatch` sur ce basculement détecte les tentatives de défusion simultanées : avec deux appels `$unmerge` parallèles pour la même Task de fusion, l'un réussit et l'autre obtient un conflit de version.
2. **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.
3. **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 :

```http
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.

![The four merge axes inverted into unmerge questions](policy-slots.svg)

**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) :

```json
{
  "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](/blog/beyond-patient-merge) a défini un `$merge` piloté par le client et a plaidé pour déplacer la politique hors du serveur. Le [deuxième](/blog/mpi-vendors-patient-merge) 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](https://www.health-samurai.io/mdmbox), une plateforme d'Index de patients maîtres et MDM native FHIR.*