> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Resolución de identidad y fusiones

> Cuando esto se entregue, la misma persona llegando desde un punto de venta, un ecommerce y un enlace de correo será un solo contacto en vez de tres.

> Traducción. Autoritativo: [`fs-core-0003-identity-resolution.md`](/modules/core/features/fs-core-0003-identity-resolution).

## Contexto

El cliente de una empresa existe en varios sistemas con llaves distintas, y un visitante anónimo se
vuelve cliente conocido en algún punto de su recorrido. Resolver eso es la razón por la que existe
un núcleo de clientes.

Es también la operación más peligrosa de la plataforma. Una fusión falsa combina dos personas reales
—su historial de compras, sus puntos, su consentimiento— y deshacerla es casi imposible porque el
estado fusionado ya se leyó, se facturó y se actuó sobre él. Toda decisión aquí se inclina a
**negarse a fusionar cuando hay incertidumbre**, y por eso las importaciones masivas producen una
cola de revisión y no una resolución (DEC-A6).

## Alcance *(normativo)*

* `core.contact_identities`: pares `(proveedor, identificador)` apuntando a un contacto.
* Promoción anónimo → conocido: un id anónimo adquiere una identidad conocida sin perder historia.
* Resolución determinista sobre identificadores verificados.
* `core.contact_merges`: registro inmutable de superviviente, absorbido y evidencia.
* Ejecución de la fusión: identidades y eventos se mueven al superviviente; el absorbido queda como
  tombstone.
* `core.contact.merged` para que los consumidores repunten sus propias referencias.

## Fuera de alcance *(normativo)*

* **Matching probabilístico o difuso.** F1 fusiona solo sobre identificadores verificados. El
  matching por similitud de nombres es como ocurren las fusiones falsas, y no vale el recall.
* Deshacer una fusión. Deliberadamente no se construye: es la operación cuya existencia fomenta
  fusionar sin cuidado. La recuperación es un restore.
* La UI de la cola de revisión — FS-CORE-0013 es dueño de la cola; esto es dueño de la fusión que
  ejecuta.

## Comportamiento *(normativo)*

1. Una identidad es `(proveedor, identificador)` único por tenant. `proveedor` nombra el sistema de
   origen — `email`, `phone`, `pos`, `ecommerce`, `national_id`.
2. Las identidades llevan un flag `verified`. **Solo las verificadas impulsan resolución
   automática.** Un email no verificado es una afirmación, no una identidad.
3. La promoción anónimo → conocido **nunca pierde historia**: la identidad anónima queda adherida al
   mismo contacto, así que los eventos recolectados antes del login siguen atribuidos.
4. La fusión automática ocurre solo con **coincidencia exacta de un identificador verificado**.
   Cualquier cosa más débil produce una *sugerencia* de fusión, nunca una fusión.
5. Una fusión es una transacción: las identidades repuntan, los eventos repuntan, el contacto
   absorbido queda como tombstone, se escribe la fila de fusión, se emite el evento y se escribe la
   auditoría.
6. El superviviente **conserva todas las identidades** de ambos lados. Perder un identificador en
   una fusión rompería la integración que lo aportó.
7. `core.contact_merges` es **append-only e inmutable**, y registra la evidencia que justificó la
   fusión. Sin evidencia, una fusión es inauditable.
8. El id del contacto absorbido sigue resolviendo — al superviviente. Una integración que guarde el
   id viejo no debe romperse.
9. Los valores en conflicto se resuelven por una política explícita y documentada (gana el no nulo
   actualizado más recientemente), y los descartados quedan registrados en la fila de fusión en vez
   de perderse.
10. PROHIBIDO: fusionar entre tenants, bajo cualquier circunstancia.

## Datos *(normativo)*

| Tabla                     | Invariantes clave                                                                                                                                        |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.contact_identities` | único (`tenant_id`, `provider`, `identifier`); FK `contact_id`; `verified` BOOL; `verified_at`; `first_seen_at`                                          |
| `core.contact_merges`     | append-only e inmutable; `survivor_contact_id`, `absorbed_contact_id`; `evidence` JSONB; `discarded_values` JSONB; `merged_by` actor tipado; `merged_at` |

## API *(normativo)*

| Endpoint                                | Clase      | Permiso               | Presupuesto |
| --------------------------------------- | ---------- | --------------------- | ----------- |
| `POST /v1/core/contacts/{id}/merge`     | Management | `core.contacts.merge` | p95 \<1 s   |
| `GET /v1/core/contacts/{id}/identities` | Management | `core.contacts.read`  | p95 \<1 s   |

## Eventos *(normativo)*

`core.contact.merged`, disponible como webhook saliente, con los ids de superviviente y absorbido
para que un consumidor repunte sus propias referencias.

## Criterios de aceptación *(normativo)*

1. Un contacto anónimo con 50 eventos, promovido por un email verificado, conserva los 50 atribuidos.
2. Una coincidencia exacta de identificador verificado fusiona automáticamente; una no verificada
   produce una sugerencia y ninguna fusión.
3. Tras la fusión el superviviente tiene la unión de ambos conjuntos de identidades, sin descartes.
4. El id del contacto absorbido sigue resolviendo al superviviente por todo camino de lectura.
5. Una fusión con falla inducida en el paso de repunte de eventos revierte por completo — no existe
   fusión parcial en ningún momento.
6. Las filas de `contact_merges` no se pueden actualizar ni borrar.
7. Los valores en conflicto descartados son recuperables desde la fila de fusión.
8. **Negativo:** un intento de fusión entre dos tenants se rechaza antes de cualquier escritura.

## Ejecución

Comando síncrono. El camino de resolución también lo llama el procesador de ingesta
(FS-CORE-0006), por lo que el servicio debe ser seguro ante concurrencia sobre el mismo contacto —
la fusión toma un row lock en ambos lados.

## Preguntas abiertas

| # | Pregunta                                                                                                  | Decide | Para             |
| - | --------------------------------------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | ¿Una coincidencia de documento es automáticamente una identidad verificada, o necesita una segunda señal? | Daniel | antes de aprobar |
| 2 | Política de conflicto de campos: ¿gana el más reciente o gana siempre el superviviente?                   | Daniel | antes de aprobar |

## Changelog

| Versión | Fecha      | Cambio           | Por qué | Autor                  |
| ------- | ---------- | ---------------- | ------- | ---------------------- |
| 0.1.0   | 2026-08-17 | Borrador inicial | —       | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
