# El modelo de evidencia

Esta es la página que explica qué hace distinto a este servicio: no sirve
números ni estados, sirve observaciones — con su evidencia, su historia y
una línea de tiempo consultable. El modelo es uno para todos los dominios;
los ejemplos alternan entre el tipo de cambio y la consulta RUC, y cada
campo con estados viene con un ejemplo por valor posible.

## El problema

Un tipo de cambio alimenta asientos contables, facturas y declaraciones; el
estado de un RUC decide si una factura se acepta. Años después, un auditor
pregunta: *¿de dónde salió este dato?* Si la API que lo sirvió solo
entregaba el valor del día, no hay respuesta: el dato pudo cambiar, la
fuente no expone histórico de sus correcciones y el rastro se perdió. Este
servicio está construido para que esa pregunta siempre tenga respuesta.

## La observación: inmutable por diseño

Cada vez que el servicio observa una fuente oficial, crea una
**observación**: qué publicó la fuente, para qué fecha, de qué fuente y con
qué consulta exacta (`identidad_consulta`), en qué instante
(`capturado_en`), con qué versión del parser, y con qué bytes crudos de
respaldo (la evidencia y, en el tipo de cambio, el `fragmento` exacto del
payload del que se parseó el valor).

**Ninguno de esos campos se edita jamás.** Si la fuente corrige un valor,
el servicio no reescribe la observación: crea una nueva que la *supersede*
y lo registra como evento. La observación corregida queda consultable para
siempre en `/v1/observations/{id}`, con su cadena completa de ancestros y
sucesoras.

- En el **tipo de cambio**, la observación es un valor (`compra`/`venta`)
  de una fecha y una serie; nace en cada captura.
- En la **consulta RUC**, la observación es el estado de un RUC en una
  edición del padrón; se materializa cuando se responde y es única por RUC y
  edición: la prueba de lo que se respondió.

### El campo `fuente`: de dónde salió

| `fuente` | qué significa |
|---|---|
| `sunat_txt` | Capturada directamente del `tipoCambio.txt` de SUNAT (serie `publicado`). La evidencia son los bytes exactos que SUNAT respondió. |
| `bcrp` | Capturada de las series estadísticas del BCRP (serie `cierre`) por un colector remoto autorizado: el paquete viaja sellado con su hash y el servicio guarda los bytes exactos de la fuente. |
| `sunat_padron_reducido` | El ZIP diario del padrón reducido de SUNAT, archivado completo (evidencia de `tipo: artefacto`); toda observación del dominio RUC sale de una edición de ese archivo. |
| `importado` | Vía excepcional para datos que una fuente solo distribuye como archivo (el archivo queda como evidencia). Siempre subordinada a la observación directa: jamás la reemplaza. |

## La revisión: la línea de tiempo del registro

Toda escritura al registro — una captura nueva, una recaptura idéntica, una
corrección, la publicación de una edición del padrón — avanza una
**revisión** global, común a todos los dominios. La revisión N es el estado
del registro tras el commit número N. Toda respuesta declara en
`dataset_revision` con qué revisión respondió, y `as_of_revision=N` congela
cualquier consulta en ella: la respuesta es siempre la misma, byte a byte.

> **Nota fechada (2026-09-01).** Entre el 2026-08-20 y el 2026-08-31, 435
> revisiones — en 22 tandas; la última, 3244–3444 — no traen ningún cambio
> de dataset: son un artefacto del incidente 2026-08-31 (la suite de
> pruebas del servicio escribía en la base productiva). Nada se borra: el
> dataset a una de esas revisiones es idéntico al de la revisión real
> anterior. Las revisiones de la vigilancia nocturna tampoco cambian el
> dataset: eso es diseño, no incidente.

### Ejemplo ilustrativo: el asiento anclado a una revisión

El caso completo de por qué guardar `dataset_revision` (escenario
ilustrativo — el registro real aún no tiene correcciones):

1. El 26/08 su sistema contable toma la venta del día para un asiento: la
   API responde `venta: "3.350"`, `observacion_id: aaaa…`,
   `dataset_revision: 731`. Su sistema guarda los tres.
2. Días después la fuente corrige ese valor. El servicio *no* edita nada:
   crea una observación nueva (`venta: "3.355"`) que supersede a `aaaa…`, en
   una revisión posterior (digamos la 810), y abre una alerta.
3. Un auditor cuestiona el asiento. Su respaldo es exacto y reproducible:
   `GET /v1/rates?currency=USD&series=publicado&from=2026-08-26&to=2026-08-26&as_of_revision=731`
   devuelve **exactamente lo que la API le respondió aquel día** —
   `venta: "3.350"`, la misma observación, byte a byte (por eso viaja con
   `ETag` y `Cache-Control: immutable`).
4. Y la misma consulta *sin* `as_of_revision` — o `/v1/observations/aaaa…`
   — muestra la corrección posterior: `superseded_por` apunta a la
   observación nueva. Usted decide si el asiento se ajusta; el servicio le
   da ambas verdades con fecha y revisión.

### Ejemplo real: un auditor en 2028 reconstruye el 1 de septiembre de 2026

Paso 1: mapear el instante a una revisión.

```sh
curl -s -H "Authorization: Bearer $EKIA_TOKEN" \
  'https://api.eurekia.pe/v1/revisions?at=2026-09-01T23:59:59-05:00'
```

<!-- ejemplo revisions: al cierre del 01/09/2026 en Lima regía la revisión 3479. -->
Respuesta real (2026-09-28, revisión 4414): al cierre del 01/09/2026 en Lima regía la revisión 3479.

```json
{
  "revision": 3479,
  "ts_commit": "2026-09-02T02:00:19.176263Z",
  "dataset_revision": 4414,
  "request_id": "8e783a05-c7a4-43df-81a1-a7c923e2f27f",
  "schema_version": "1.0"
}
```
<!-- /ejemplo -->

Paso 2: consultar anclado a esa revisión. En el tipo de cambio,
`/v1/rates?…&as_of_revision=3479` devuelve solo las fechas que el registro
conocía entonces — ni más ni menos. En el padrón, `/v1/ruc/{ruc}?as_of_revision=3479`
responde `NO_DATA_AT_REVISION`: el dominio RUC publicó su primera edición
después de esa revisión, y el servicio lo dice en lugar de inventar un
estado ([la forma en Consulta RUC](consulta-ruc.md#consulta-historica)).

## Las marcas: qué le pasó a una observación (tipo de cambio)

`marcas` lista los eventos que la observación acumuló hasta la revisión
consultada. Un escenario por valor:

| marca | escenario que la produce |
|---|---|
| `capturada` | La corrida de las 02:30 de Lima encuentra el dato del día por primera vez y crea la observación con su evidencia. Toda observación directa de fuente la lleva. |
| `recaptura_identica` | Las corridas siguientes del día vuelven a consultar la fuente (siempre consultan) y encuentran el mismo valor canónico: no se crea observación nueva, pero cada re-consulta queda registrada con su propia evidencia. La observación real del 21/08 (`f39403ae-…`) lleva esta marca por sus recapturas — prueba de vigilancia continua. |
| `sin_cotejo` | El `publicado` del día D se capturó antes de que existiera el `cierre` del día hábil anterior (la fuente lo publica por la tarde): todavía no había contraparte que cotejar. El cotejo corre solo cuando llega el cierre; la marca queda como constancia de que se intentó. Real en la respuesta de [Empezar](empezar.md#primer-request): capturada de madrugada, `sin_cotejo` hasta la tarde. |
| `supersedida` | Una captura posterior trajo un valor distinto para la misma fecha/serie: corrección de la fuente. La observación queda en la historia con esta marca y `superseded_por` apunta a su sucesora (ilustrativo — aún no ha ocurrido en el registro real). |
| `plausibilidad_baja` | El valor capturado varía frente al día anterior más de lo plausible (umbral configurable; 5 % por defecto). Ilustrativo: la venta pasa de 3.350 a 3.550 de un día a otro. La observación *se crea igual* — el servicio no descarta datos — pero queda marcada y con alerta abierta: usted decide si la usa. |
| `reproceso` | La evidencia se guardó pero el parser de entonces no pudo procesarla (formato inesperado); una versión corregida del parser la reprocesó después. La observación nace con el `capturado_en` original — sin huecos irrecuperables. |

## Las transiciones: qué le pasó a un RUC (consulta RUC)

El padrón no tiene marcas ni supersesión: SUNAT publica cada día la edición
entera, y el servicio la compara con la anterior. Cada diferencia por RUC
es una **transición** (`estado_anterior` → `estado_nuevo`, los dos
snapshots, `campos_cambiados`), con la revisión desde la que existe. Una
edición corregida por SUNAT no reescribe la anterior: aparece como una
transición más, en la edición siguiente. La historia completa de un RUC
(`/v1/ruc/{ruc}/history`) es su estado inicial más sus transiciones —
[la forma en Consulta RUC](consulta-ruc.md#consulta-historica).

## Los controles de integridad

"Controles de integridad" es el paraguas de todo mecanismo con que el
servicio contrasta lo que sirve. Cada dominio tiene el suyo, con su alcance
declarado en su página:

| dominio | control | qué detecta | qué no detecta |
|---|---|---|---|
| tipo de cambio | **cotejo** entre fuentes (`cotejos[]`, `tiene_discrepancia_abierta`): el `publicado` de cada día contra el `cierre` del último día hábil anterior — el mismo número por construcción. | Errores de transmisión, parseo o publicación en cualquiera de las dos cadenas de captura. | Un error aguas arriba, común a ambas fuentes (comparten origen: la tabla de la SBS). |
| consulta RUC | **confiabilidad** por verificación exhaustiva (`confiabilidad`): la edición reconstruida desde el baseline y las transiciones se contrasta RUC por RUC contra el artefacto original, tras cada publicación. Más `dv_valido`, el dígito verificador de cada RUC. | Cualquier diferencia entre lo que el servicio sirve y lo que SUNAT publicó. | Un error en el padrón mismo: **RUC no tiene cotejo** porque la fuente es única; una corrección de SUNAT llega como transición, no como supersesión. |

**El cotejo, el día normal** — ambas fuentes coinciden (cotejo real del
`publicado` del 25/08/2026 contra el `cierre` del 24/08/2026):

```json
"cotejos": [
  {
    "cotejo_id": "5c3d6be3-f5cd-4633-b5ee-193523d6791a",
    "contraparte": "3b6f2c09-6897-4730-a84e-1fe72775cbff",
    "resultado": "ok",
    "regla": "publicado_vs_cierre_prev_habil-1.0",
    "revision": 2843,
    "ts": "2026-08-28T22:00:26.894339Z"
  }
],
"tiene_discrepancia_abierta": false
```

**El cotejo, el día discrepante** — las fuentes no coinciden (ilustrativo:
aún no ha ocurrido en el registro real):

```json
"cotejos": [
  {"contraparte": "1f2e3d4c-…", "resultado": "discrepante",
   "regla": "publicado_vs_cierre_prev_habil-1.0", "revision": 995}
],
"tiene_discrepancia_abierta": true
```

Ante una discrepancia el servicio *no arbitra* cuál fuente manda: la expone
y deja la decisión al consumidor. La decisión típica: un sistema contable
conservador retiene el dato marcado hasta que un cotejo posterior vuelva a
`ok` o un humano decida con constancia. `tiene_discrepancia_abierta` es el
derivado ya calculado: existe un cotejo discrepante posterior al último
`ok` contra la misma contraparte.

**La confiabilidad** tiene tres valores. `verificada` significa exactamente
esto: la edición reconstruida desde el baseline y las transiciones se
contrastó RUC por RUC (18,4 millones de comparaciones) contra el artefacto
original. Con el levantamiento exhaustivo tras cada publicación,
**`verificada` es el estado normal de toda edición publicada**;
`sin_verificar` es la ventana entre publicar y terminar ese levantamiento;
`fallida` es una inconsistencia conocida, con `alerta_ref`. La respuesta
real de [Empezar](empezar.md#primer-request) lo muestra.

## La supersesión: correcciones sin reescribir el pasado

Dos campos del ítem de tipo de cambio cuentan la historia de vigencia, y no
significan lo mismo:

- `superseded` es **relativo a la revisión consultada**: `false` significa "a
  esa revisión, este era el valor vigente". En un ítem de `/v1/rates` siempre
  es `false` por construcción.
- `superseded_por` mira **el presente**: si el dataset actual ya corrigió esa
  observación, aquí aparece el UUID de la sucesora.

La cadena completa vive en `/v1/observations/{id}`: `cadena.ancestros` (las
observaciones que esta corrigió) y `cadena.sucesoras` (las que la corrigieron
a ella). Las correcciones son encadenables sin límite: A supersedida por B, B
por C — y las tres siguen consultables para siempre.

## Sin relleno: nunca otra fecha como si fuera la pedida

El principio es uno: si una fecha no tiene dato, la API lo dice, con la
razón del hueco, y **jamás entrega otra fecha en lugar de la pedida**. Si su
sistema recibe un valor, es de la fecha que pidió. La forma cambia por
dominio:

| dominio | fecha sin dato | el hueco y su razón |
|---|---|---|
| tipo de cambio | `404 NO_OBSERVATIONS` — ningún ítem, ningún sustituto ([forma](tipo-de-cambio.md#consulta-historica)). | `/v1/coverage`: `huecos[].razon` ∈ `fallo` · `nd_fuente` · `sin_dato`. |
| consulta RUC | `200` con **solo metadatos** — `cobertura {efectiva: false, razon}` y las ediciones vecinas, sin ningún campo de estado; usted elige la vecina y la pide explícitamente ([forma](consulta-ruc.md#consulta-historica)). | `/v1/ruc-coverage`: una fila por fecha con `razon` (`no_capturada`, `no_durable`, `rechazada`, `retenida_*`, `excede_capacidad_v1`, `no_esperada`). |

## La evidencia: verifique el hash usted mismo

Cada captura guarda los **bytes exactos** que la fuente respondió y su
SHA-256. Un solo contrato para toda evidencia: `/v1/evidence/{id}` da los
metadatos (`tipo`, `sha256`, `bytes`, `fuente`, `fecha`, `descarga_url`) y
`/v1/evidence/{id}/descarga` los bytes — directos para una captura pequeña
(`tipo: observacion`, con `ETag` = sha256), por redirect firmado para un
artefacto grande (`tipo: artefacto`, el ZIP del padrón). La receta completa,
ejecutada real en los dos dominios:
[Verifique la evidencia en 60 segundos](verificar-evidencia.md).

## ¿Qué garantiza «inmutable»?

Tres capas, cada una con su alcance declarado — la honestidad del límite es
el argumento de confianza, no su debilidad:

1. **La aplicación no puede editar.** Los roles de base de datos del
   servicio no tienen privilegios `UPDATE` ni `DELETE` sobre las tablas del
   registro: observaciones, eventos, evidencia, transiciones y revisiones
   solo se insertan. No es una política de código — es un permiso revocado
   en la base, probado por los tests del servicio.
2. **El administrador de la base de datos podría — y se declara.** Un
   administrador con acceso directo podría alterar filas; ese límite está
   escrito en la especificación del servicio en vez de escondido. El control
   compensatorio son las copias de seguridad externas, versionadas y sin
   borrado desde el servidor, y el archivo de artefactos en un bucket con
   bloqueo de objetos: contra ellos se detecta una alteración local.
3. **Usted puede verificar sin confiar en nadie.** Cada respuesta que su
   sistema guardó se verifica por hash: la evidencia por su SHA-256, las
   respuestas ancladas a revisión por su `ETag`, y el servicio registra el
   hash de cada body servido — ante un body que usted aporte, puede probarse
   su identidad e integridad.

**Retención: indefinida.** Ni las observaciones ni la evidencia se borran.
Los artefactos del padrón viven en un archivo inmutable con un bloqueo
mínimo renovable de 1095 días (el bloqueo impide borrar incluso a quien
tenga la clave; se renueva antes de vencer): el bloqueo es el mínimo, no el
plazo de retención.

## Tabla de estados completa

| campo | valores | dónde aparece |
|---|---|---|
| `fuente` | `sunat_txt` · `bcrp` · `sunat_padron_reducido` · `importado` | Ítems de tasas, `/v1/observations/{id}`, metadatos de `/v1/evidence/{id}` |
| `marcas` | `capturada` · `recaptura_identica` · `sin_cotejo` · `supersedida` · `plausibilidad_baja` · `reproceso` | Ítems de tasas; eventos completos en `/v1/observations/{id}` |
| `cotejos[].resultado` | `ok` · `discrepante` | Ítems de tasas y `/v1/observations/{id}` |
| `tiene_discrepancia_abierta` | `true` · `false` | Ítems de tasas — derivado ya calculado de los cotejos |
| `superseded` | `true` · `false` (relativo a la revisión consultada) | Ítems de tasas y `/v1/observations/{id}` |
| `superseded_por` | UUID de la sucesora · `null` (mira el dataset actual) | Ítems de tasas y `/v1/observations/{id}` |
| `frescura.estado` | `al_dia` · `atrasada` · `adelantada` (solo RUC), con `dias_atraso` y `fecha_esperada` | `/v1/rates/latest`, `/v1/ruc/{ruc}`, `/v1/ruc-coverage` — reglas en cada [servicio](tipo-de-cambio.md#frescura) |
| `huecos[].razon` | `fallo` · `nd_fuente` · `sin_dato` | `/v1/coverage` — [Tipo de cambio](tipo-de-cambio.md#cobertura) |
| `cobertura[].razon` (alias `causa`) | `no_capturada` · `no_durable` · `rechazada` · `retenida_pendiente` · `retenida_excepcional_pendiente` · `retenida_caducada` · `excede_capacidad_v1` · `no_esperada` | `/v1/ruc-coverage` y el bloque `cobertura` de `as_of_fecha` — [Consulta RUC](consulta-ruc.md#cobertura) |
| `estado_fila` | `presente` · `no_interpretable` · `ausente` · `ausente_incierto` | `/v1/ruc/{ruc}` y los snapshots de la historia — [Consulta RUC](consulta-ruc.md#que-es-y-que-no) |
| `confiabilidad` | `verificada` · `sin_verificar` · `fallida` (con `alerta_ref`) | `/v1/ruc/{ruc}`; por tramo y global en `/v1/ruc/{ruc}/history` |
| `dv_valido` | `true` · `false` | `/v1/ruc/{ruc}` y su historia |
| `tipo` (evidencia) | `observacion` · `artefacto` | `/v1/evidence/{id}` |
| `code` (errores) | tabla completa, por dominio | Todo error — [Errores y límites](errores-y-limites.md#los-codigos) |

## Los límites, declarados

- El alcance de cada control de integridad está declarado arriba: el cotejo
  no ve un error común a ambas fuentes; la confiabilidad no ve un error del
  padrón mismo.
- El alcance exacto de "inmutable" está declarado capa por capa.
- Una corrección de la fuente posterior a la última consulta del día solo se
  detecta de forma indirecta (por cotejo posterior, o por la transición de
  la edición siguiente): las fuentes no exponen su propio histórico.

## Qué guardar en su sistema

Con cada dato que tome de la API, guarde lo que permite reconstruirlo años
después — qué observación lo respaldaba, qué evidencia tenía y qué contenía
el registro cuando lo tomó:

| dominio | guarde |
|---|---|
| todos | `observacion_id` y `dataset_revision`. |
| consulta RUC | además `edicion.ordinal` y `edicion.fecha_padron` (qué edición del padrón se consultó) y, si va a re-verificar el artefacto, `edicion.sha256`. |
