---
{
  "title": "Diseñando $unmerge: cómo revertir la fusión de pacientes en FHIR",
  "description": "FHIR no dispone de la operación $unmerge. Nosotros la hemos construido: revierte una fusión mediante la API Provenance + History y permite que el cliente decida qué hacer con los datos añadidos entre la fusión y la reversión.",
  "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 refleja la interfaz de $merge: el servidor es responsable de los invariantes (atomicidad, auditoría, ciclo de vida) y el cliente es responsable de la política. La parte sencilla es restaurar el estado previo a la fusión: Task, Provenance y la History API nos proporcionan todo lo necesario para construir un Bundle de transacción inverso. La parte difícil es la brecha temporal: los datos añadidos entre la fusión y la reversión.",
  "utm-campaign": "fhir_expert",
  "utm-content": "unmerge-design"
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

Este es el tercer y último artículo de la serie sobre fusiones en MDMbox. El [primero](/blog/beyond-patient-merge) presentó nuestro diseño orientado al cliente para la operación `$merge`: el cliente envía un Bundle de transacción que describe cada PUT, POST y DELETE; el servidor lo envuelve con Task + Provenance y lo ejecuta de forma atómica. El [segundo](/blog/mpi-vendors-patient-merge) explicó por qué la fusión no puede ser un único algoritmo: existen al menos cuatro ejes de política independientes, cada uno con múltiples variantes implementadas en proveedores de MPI, plataformas MDM, HCE y registros nacionales. Y aquí hablaremos finalmente de la operación contraria, `$unmerge`.

La especificación FHIR nos ofrece `Patient/$merge` en nivel de madurez 0 y ningún `$unmerge` en absoluto; sin embargo, todo MPI y MDM en producción debe admitir la reversión, porque todo Índice Maestro de Pacientes acaba fusionando dos registros que no deberían haberse fusionado. La pregunta no es si una plataforma MDM necesita la operación de reversión, sino cómo debe comportarse dicha reversión.

## Por qué $unmerge no es simplemente $merge al revés

La tentación es tratar la reversión como «ejecutar la transacción de fusión hacia atrás». Leer el Provenance, obtener las versiones previas a la fusión desde el historial y volver a escribirlas. Listo.

Funciona para la mitad mecánica del problema, pero no para la mitad de política. Veamos un par de ejemplos.

**Brecha temporal.** Anna Schmidt está registrada dos veces como `Patient/anna-old` y `Patient/anna-new`. El lunes, `Patient/anna-old` se fusiona en `Patient/anna-new`, registrándose como `Task/merge-anna` con un Provenance asociado. El martes, se publican tres nuevos Encounters en `Patient/anna-new` (`Encounter/visit-tue-1..3`), visitas que la clínica registró después de unificar la identidad. El miércoles se solicita una reversión porque la fusión original fue incorrecta. Esos tres Encounters no tienen versión previa a la fusión. Nunca estuvieron en `Patient/anna-old`. ¿Adónde van ahora: de vuelta al origen restaurado, se mantienen en el destino, se distribuyen según alguna regla clínica o se envían a una cola de supervisión?

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

No existe un único algoritmo definitivo para esto. Cada registro de Encounter podría pertenecer a cualquiera de los dos pacientes, según cuál de ellos realizó realmente la visita, y el servidor no lo sabe.

**Eliminación definitiva.** Si el paciente origen fue eliminado definitivamente en el momento de la fusión (el patrón VistA descrito en nuestro [artículo anterior](/blog/mpi-vendors-patient-merge)), el registro origen ya no existe en las tablas activas. La History API aún conserva sus versiones anteriores, pero recrear un recurso activo a partir de una marca de eliminación es una decisión de política: ¿se crea un nuevo ID y se preserva el mapeo en la pista de auditoría de la reversión, se restaura el ID original aceptando la brecha, o se rechaza la reversión por completo? Las distintas implementaciones MDM responden a esto de manera diferente por razones que no tienen nada que ver con FHIR: normas de retención, suscriptores de flujos descendentes, requisitos de auditoría jurisdiccionales.

Ese límite determina la interfaz: $unmerge es un plan de reversión suministrado por el cliente, no un comando de deshacer en el lado del servidor.

El cliente envía la Task de fusión original y un Bundle de transacción que describe el estado restaurado deseado. El servidor valida ese plan, comprueba los bloqueos optimistas, lo aplica de forma atómica y escribe la pista de auditoría mediante Task/Provenance.

La operación `$unmerge` es, por tanto, simétrica a `$merge` según describimos en nuestro [primer artículo](/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\""}
          }
        ]
      }
    }
  ]
}
```

El único dato de entrada obligatorio es una referencia a la Task de fusión original. Todo lo demás (qué versión de qué recurso se restaura y dónde, qué ocurre con los datos añadidos tras la fusión, qué hacer con el origen si fue eliminado) reside en el Bundle `plan`.

`preview: true` realiza una ejecución en seco del mismo flujo de validación. El servidor devuelve el OperationOutcome que habría devuelto y el Provenance resultante, de modo que el cliente puede mostrar al supervisor exactamente qué tocará la reversión antes de autorizarla.

Las cabeceras `ifMatch` gestionan el bloqueo optimista, igual que en la fusión. Si un recurso cambia entre el momento en que el cliente construye el plan de reversión y el momento en que el servidor lo ejecuta, la transacción falla. No sobreescribimos silenciosamente trabajo que el supervisor no ha visto.

## Construyendo el plan a partir de Provenance

El cliente no tiene que construir el plan desde cero. El Provenance de fusión ya contiene los datos que el cliente necesita.

Para cada recurso que tocó la fusión, el Provenance de fusión registró una referencia con versión en `entity[].what`, por ejemplo `Patient/anna-old/_history/2`. Ese es el estado previo a la fusión. La History API lo resuelve directamente:

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

Un plan predeterminado ingenuo consiste en una transformación de un solo paso:

1. Leer la Task de fusión, seguir sus listas `Provenance.target` y `Provenance.entity`.
2. Para cada recurso afectado, obtener su versión previa a la fusión desde el historial.
3. Emitir una entrada `PUT` que restaure esa versión, con `ifMatch` referenciando la versión *actual* del recurso (para detectar ediciones concurrentes).
4. Para recursos creados durante la fusión que no tenían estado previo (normalmente: un registro Linkage o una aserción del comparador), emitir un `DELETE`. Los valores copiados en un recurso existente (por ejemplo, identificadores añadidos al Patient destino) desaparecen mediante el `PUT` de restauración de ese recurso.

La mayor parte de la interfaz de supervisión de MDMbox utiliza exactamente este comportamiento predeterminado. El supervisor ve el Bundle inverso propuesto, edita las entradas que requieren decisiones de política y lo envía.

Lo que el comportamiento predeterminado *no* gestiona es la brecha temporal. Los recursos creados o modificados *después* de la marca de tiempo de la fusión no están en el Provenance de fusión. Son estado nuevo neto, y los cuatro puntos de decisión de política que se describen a continuación son el mecanismo mediante el cual el supervisor indica qué hacer con ellos.

## La cadena de auditoría

La reversión no borra la fusión: la amplía. Tras confirmar `$unmerge`, coexisten tres artefactos para `Task/merge-anna`:

1. **La Task de fusión original**, con `businessStatus` cambiado de `merged` a `unmerged` dentro de la misma transacción. El `ifMatch` en ese cambio detecta intentos de reversión concurrentes: ante dos llamadas `$unmerge` paralelas para la misma Task de fusión, una tiene éxito y la otra recibe un conflicto de versión.
2. **El Provenance de fusión**, intacto. Sigue nombrando las versiones previas a la fusión en `entity[].what`. La reversión no invalida la pista de auditoría de la fusión; se construye sobre ella.
3. **Una nueva Task de reversión con `basedOn → Task/merge-anna`**, asociada a un Provenance de reversión que apunta a las versiones *previas a la reversión* de todo lo que tocó el plan inverso.

La cadena se lee mediante una consulta hacia delante:

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

Esta última llamada es la que auditores y supervisores ejecutan en la práctica cuando el origen se restaura bajo su ID original. `Patient/anna-old/_history` se lee como una narrativa continua: creado, existió, eliminado en la fusión, recreado en la reversión, existió de nuevo. Cada transición tiene un Provenance asociado, cada Provenance cita una Task, cada Task de reversión cita la Task de fusión que revirtió. Si un despliegue crea un nuevo ID en lugar del original, la continuidad queda explícita en el Provenance de reversión en vez de en un único historial del Patient.

El mismo esquema se aplica de forma recursiva. Si `Patient/anna-old` vuelve a fusionarse el mes siguiente y se revierte de nuevo, la línea temporal crece con otro ciclo de fusión-reversión anclado a su propio par de Tasks. Nada colapsa, nada se reescribe: la History API es el registro de auditoría desplegado.

## Cuatro puntos de decisión de política: los ejes de fusión, invertidos

Un punto de decisión de política es el lugar en el plan de reversión donde el cliente debe tomar una decisión explícita. Algunos puntos modifican el propio Bundle de transacción. Otros generan trabajo de seguimiento para supervisores, comparadores o sistemas descendentes. En conjunto, hacen que el estado restaurado sea intencionado en lugar de meramente reversible desde el punto de vista histórico.

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

**Supervivencia → reasignación de campos.** La fusión preguntaba qué valor prevalece cuando dos registros entran en conflicto. La reversión pregunta: para los campos escritos *después* de la fusión, ¿a qué registro restaurado se asignan? Un nuevo número de teléfono registrado el martes: ¿va al origen, al destino o a ambos? MDMbox trata esto como una decisión del cliente campo a campo, con un comportamiento predeterminado de «permanecer en el destino» (el registro que estaba activo cuando se escribió el campo) más un indicador en el Provenance para que un supervisor pueda revisarlo.

**Gestión de referencias → reasignación de referencias.** La fusión preguntaba qué hacer con los punteros al origen. La reversión pregunta: ¿adónde apuntan ahora las *nuevas* referencias, es decir, las creadas después de la fusión que siempre apuntaban al destino? Una reclamación presentada el martes que referencia `Patient/anna-new` tendrá ahora dos pacientes en el ámbito, y la especificación no ofrece orientación al respecto. Hacemos de esto una decisión explícita en el `plan` por tipo de referencia. El comportamiento predeterminado es mantener las nuevas referencias en el destino, salvo que el cliente lo indique de otro modo.

**Disposición del origen → restauración del origen.** La fusión preguntaba qué ocurre con el origen. La reversión pregunta: ¿qué regresa? Si el origen se mantuvo inactivo con `replaced-by`, la restauración es mecánica: se vuelve `active` a verdadero y se elimina el enlace. Si el origen fue eliminado definitivamente, el cliente debe elegir: restaurar el ID original desde el historial (aceptando la brecha de versión), crear un nuevo ID con un Provenance de reversión que mapee el origen versionado antiguo al nuevo ID del Patient, o rechazar la reversión.

**Efectos descendentes → cola de reevaluación.** La fusión indicaba que los cálculos acumulativos y los grafos de enlaces necesitaban reevaluación. La reversión dice lo mismo, con un matiz adicional: todo lo *construido* durante el período fusionado (dosis de radiación recalculada, pertenencia a cohortes recomputada, cachés de terceros, el propio grafo de enlaces probabilístico del MPI) ya no puede considerarse fiable sin revisión para ninguno de los dos pacientes restaurados. La Task de reversión y su Provenance son el ancla de suscripción: los consumidores descendentes (interfaz de supervisión, recalculadores de dosis acumulada, el comparador) observan `Task?code=unmerge` y reevalúan los recursos listados en `Provenance.target`. El servidor publica el evento de auditoría; qué recomputar es una decisión descendente.

El patrón es el mismo que en `$merge`. El servidor no dispone del contexto clínico para tomar estas decisiones. El lado cliente es quien debe tomarlas.

## Tras la reversión: nueva fusión y retroalimentación al comparador

Una vez que `$unmerge` confirma la transacción, el `businessStatus` de la Task de fusión muestra `unmerged`. La comprobación previa a la fusión de MDMbox (la consulta que impide fusionar un origen ya fusionado) busca `Task?code=merge&business-status=merged&subject={ref}` y ya no encuentra la Task anterior. `Patient/anna-old` es elegible para una nueva fusión. Esto es intencionado: la reversión invierte completamente el ciclo de vida de la fusión, incluido su bloqueo sobre una nueva fusión.

Si un humano acaba de decidir que dos pacientes son personas distintas, el comparador no debería volver a fusionarlos silenciosamente en la siguiente ejecución por lotes. El razonamiento en contra de automatizarlo es sólido: una reversión puede estar deshaciendo un error *técnico* (identificadores incorrectos en el plan de fusión, un origen que apunta al registro equivocado) y no una afirmación *clínica* (realmente son personas distintas). El servidor no puede distinguir cuál es el caso.

MDMbox mantiene las dos señales separadas. La Task de reversión y el Provenance son el registro público de la reversión, consultable por el comparador, la interfaz de supervisión o cualquier suscriptor descendente. Una aserción de «no fusionar», cuando un despliegue MDM la necesita, es responsabilidad del cliente escribirla; y como el plan de reversión es un Bundle de transacción estándar, el cliente añade un `POST` para esa aserción junto a las entradas del plan inverso.

El portador FHIR natural es un recurso `Linkage` que apunta a ambos pacientes restaurados, con el código «no fusionar» del despliegue en una extensión (los códigos `Linkage.item.type` integrados, como `source`/`alternate`/`historical`, no cubren aserciones del comparador):

```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"}
}
```

Una alternativa en el propio recurso es `Patient.link` con `type=seealso` más la misma extensión en el enlace: menos recursos, pero el estado del comparador acaba mezclado con los datos demográficos en la cadena de versiones del Patient. La mayoría de los despliegues preferirán el `Linkage` independiente para que las reglas «no fusionar» del comparador residan en su propio recurso y puedan consultarse, caducar o revocarse sin tocar las versiones del Patient.

Tiene la misma atomicidad que todo lo demás: la reversión y la aserción se confirman juntas, o ninguna lo hace. El contrato del servidor termina en el límite del Bundle; la forma que adopta la aserción y qué registros cubre es decisión del cliente.

El mismo principio se aplica a las cadenas de fusión. Un destino puede haber recibido más de una fusión (`A → B`, y luego `C → B`); el servidor no lo impide. Cuando el cliente revierte una de ellas posteriormente, el plan inverso debe tener en cuenta que el estado actual de `B` refleja *ambas* fusiones, no solo la que se está revirtiendo. Cada fusión tiene su propio Provenance, por lo que los datos están disponibles, pero aplicarlos correctamente a través de fusiones solapadas es una decisión del cliente, no del servidor.

Algunas reversiones también necesitan una comprobación de política antes de que se permita confirmar el plan inverso. El caso canónico es el estado clínico acumulativo: dosis de radiación de por vida, totales acumulados de opioides o cualquier cálculo firmado que quedaría invalidado al redistribuir eventos entre dos pacientes. El contrato portable no es «rechazar siempre» ni «proceder siempre». La Task de fusión es el punto de anclaje para ese bloqueo; la vista previa puede mostrarlo como un OperationOutcome; el despliegue decide si la reversión queda bloqueada en línea, se enruta a una cola de revisión humana o se permite mientras los totales derivados permanecen en cuarentena.

## Cerrando la serie

Este artículo completa la trilogía sobre fusiones. El [primero](/blog/beyond-patient-merge) definió un `$merge` orientado al cliente y argumentó a favor de trasladar la política fuera del servidor. El [segundo](/blog/mpi-vendors-patient-merge) demostró que la política no es una única decisión, sino cuatro, y que cada proveedor de MPI en producción implementa una combinación diferente. Este último refleja el contrato para la dirección inversa: la misma interfaz, los mismos cuatro ejes invertidos en preguntas de reversión, la misma división del trabajo entre invariantes del servidor y política del cliente.

---

*La implementación completa de $merge / $unmerge está disponible hoy en [MDMbox](https://www.health-samurai.io/mdmbox), una plataforma MDM e Índice Maestro de Pacientes nativa en FHIR.*