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

# DSL de segmentos y evaluación incremental

> Cuando esto se entregue, "clientes que gastaron más de 500 000 en los últimos 90 días" será una audiencia viva que se actualiza segundos después de una compra, no un reporte nocturno.

> Traducción. Autoritativo: [`fs-core-0008-segments.md`](/modules/core/features/fs-core-0008-segments).

## Contexto

Los segmentos son lo que convierte eventos almacenados en algo sobre lo que un marketero puede
actuar, y la diferencia entre un segmento que se actualiza en segundos y uno que se actualiza de un
día para otro es la diferencia entre una campaña que llega durante una venta flash y una que llega
después.

ADR-012 hace un canje deliberado: el DSL está **restringido para que la evaluación incremental sea
posible**. Un lenguaje de consulta general sería más expresivo y obligaría a recalcular todo con
cada evento. Restringir la gramática a condiciones cuya verdad solo puede cambiar de formas conocidas
significa que un evento toca únicamente los segmentos que realmente puede afectar.

Ser honesto sobre el límite es parte del diseño: un segmento que no se puede evaluar
incrementalmente se marca `nightly_only` **al crearse**, para que quien lo construye lo sepa antes de
depender de él.

## Alcance *(normativo)*

* El DSL JSON versionado: atributos de perfil tipados, agregados de eventos sobre una ventana,
  pertenencia a otros segmentos y niveles, AND/OR/NOT.
* Evaluación incremental en cada evento registrado.
* `core.segment_members` con historia de entrada y salida.
* `core.segment.entered` y `core.segment.exited`.
* Detección de `nightly_only` al crear, expuesta en el constructor.
* Recálculo completo por tenant como red de seguridad, al tier de frescura.
* Plantillas RFM preconstruidas por taxonomía vertical.

## Fuera de alcance *(normativo)*

* La UI del constructor de segmentos — `frontend/console`.
* Enviar a un segmento — `messaging`.
* Segmentos predictivos o derivados de ML. No planificado: son inexplicables para quien tiene que
  defender la campaña.
* Extender el DSL ad hoc. Un tipo de nodo nuevo es un cambio versionado con su propio spec.

## Comportamiento *(normativo)*

1. El DSL es **versionado**. Un segmento registra la versión del DSL contra la que se escribió, de
   modo que un cambio de gramática nunca reinterpreta silenciosamente una definición existente.
2. La gramática es cerrada: comparaciones de atributos tipados, agregados de eventos (`count`,
   `sum(propiedad)`, `first_seen`, `last_seen`) sobre últimos N días o todo el tiempo, pertenencia a
   otro segmento o nivel, y AND/OR/NOT. PROHIBIDO: SQL arbitrario, código de usuario, joins no
   acotados.
3. Las referencias entre segmentos son **acíclicas**, verificado al crear. Un ciclo recalcularía
   para siempre.
4. La evaluación es incremental: un evento evalúa solo los segmentos cuyas condiciones referencian
   ese nombre de evento o los atributos que cambió.
5. Los cambios de membresía emiten `entered` o `exited`. Reevaluar sin cambio no emite **nada** — un
   segmento que se redispara con cada evento haría inusables los triggers de campaña.
6. `core.segment_members` conserva historia con `entered_at` y `exited_at`. "¿Este contacto estaba en
   el segmento VIP cuando enviamos ese mensaje?" tiene que ser respondible.
7. Una definición que no se puede evaluar incrementalmente se marca `nightly_only` al crearse y **el
   constructor se lo dice al usuario**. Descubrirlo después, con datos rancios, es la falla que esta
   regla previene.
8. El recálculo completo corre por tenant al tier de frescura (nocturno por defecto, DEC-G5) como red
   de seguridad, y su divergencia con el estado incremental se **alerta, no se corrige en silencio**.
9. Editar una definición crea una versión nueva y dispara un recálculo completo de ese segmento; la
   membresía bajo la versión anterior queda en la historia.

## Datos *(normativo)*

| Tabla                  | Invariantes clave                                                                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.segments`        | `tenant_id`; `name`; `definition` JSONB; `dsl_version`; `nightly_only` BOOL; `version` entero; `is_active`                                         |
| `core.segment_members` | (`segment_id`, `contact_id`, `entered_at`) con `exited_at` nullable; historia append-only; a lo más una membresía abierta por (segmento, contacto) |

## API *(normativo)*

| Endpoint                             | Clase      | Permiso                                | Presupuesto            |
| ------------------------------------ | ---------- | -------------------------------------- | ---------------------- |
| `GET/POST/PATCH /v1/core/segments`   | Management | `core.segments.{read\|create\|update}` | p95 \<1 s              |
| `POST /v1/core/segments/preview`     | Management | `core.segments.preview`                | p95 \<5 s, solo conteo |
| `GET /v1/core/segments/{id}/members` | Management | `core.segments.read`                   | p95 \<1 s, cursor      |

`preview` devuelve un conteo y una muestra, nunca la membresía completa, y su presupuesto es
deliberadamente más holgado: es una herramienta de diseño, no un camino de runtime.

## Eventos *(normativo)*

`core.segment.entered` y `core.segment.exited`, ambos disponibles como webhooks salientes y ambos
disparadores de campaña en `messaging`.

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

1. Una compra que cruza un umbral de segmento emite `entered` en \<5 s p95.
2. Reevaluar una membresía sin cambios no emite nada.
3. Cada tipo de nodo del DSL tiene un fixture que prueba que su diff incremental es correcto.
4. Una definición con una condición no evaluable incrementalmente se marca `nightly_only` al crearse,
   y la respuesta lo dice.
5. Una referencia cíclica entre segmentos se rechaza al crear.
6. El recálculo completo reconcilia un drift sembrado deliberadamente y alerta en vez de corregirlo
   en silencio.
7. La historia de membresía responde correctamente "¿estaba el contacto X en el segmento Y en la
   fecha Z?".
8. **Negativo:** editar una definición no reescribe las filas históricas de membresía.

## Ejecución

Pipeline asíncrono. Evaluación en `backend/workers` dentro de la pasada del procesador de ingesta;
recálculo completo en `backend/scheduler`. El evaluador del DSL vive en `packages/core` como función
pura con fixtures exhaustivos por nodo.

## Preguntas abiertas

| # | Pregunta                                                                                      | Decide | Para             |
| - | --------------------------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | Profundidad máxima de anidamiento y cantidad de nodos por definición: ¿dónde ponemos el tope? | Daniel | antes de aprobar |
| 2 | ¿Las plantillas RFM llegan con la taxonomía vertical o como un set instalable aparte?         | 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.*
