|
15 min de lectura
|

Diseñando $unmerge: cómo revertir la fusión de pacientes en FHIR

Resumen del artículo

$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.

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok
How many duplicates are in your patient data?

Most teams can't answer this. A 45-minute call with our engineers will get you close.

Tell us how patient records get created and move between your systems. We'll point out where duplicates usually come from in setups like yours, give a rough estimate of your duplicate rate, and say honestly whether you need an MPI. Sometimes the answer is "not yet" — we'll tell you that too.

Free · 45 minutes · you talk to an engineer, not sales

Este es el tercer y último artículo de la serie sobre fusiones en MDMbox. El primero 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 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

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), 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.

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\""}
          }
        ]
      }
    }
  ]
}

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:

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:

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

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

{
  "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 definió un $merge orientado al cliente y argumentó a favor de trasladar la política fuera del servidor. El segundo 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, una plataforma MDM e Índice Maestro de Pacientes nativa en FHIR.

Compartir este artículo
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

Get the latest articles on FHIR, interoperability, and healthcare IT.