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

# Registro de auditoría

> Cuando esto se entregue, todo cambio de estado de la plataforma será rastreable hasta quién lo hizo, de qué a qué, y qué request lo causó.

> Traducción. Autoritativo: [`fs-core-0005-audit-log.md`](/modules/core/features/fs-core-0005-audit-log).

## Contexto

ADR-017 eligió CQRS-lite sobre event sourcing, y el registro de auditoría es lo que hace honesto ese
canje: sin un registro completo de cambios, descartar event sourcing significaría renunciar a poder
responder "por qué esta fila está así".

Tiene un segundo trabajo. La Ley 21.719 fiscaliza **evidencia operativa**, y el registro es la
evidencia: quién accedió a un documento de identidad, cuándo cambió un consentimiento, qué operador
suplantó a qué tenant. Por eso se registran también las lecturas de acceso, no solo las escrituras.

Se entrega temprano porque no depende de nada y todo depende de él. Un command handler escrito antes
de que exista el registro se escribirá sin él.

## Alcance *(normativo)*

* `core.audit_log`, append-only, particionada RANGE mensual.
* Actor tipado: `user | member | api_key | system`, con id.
* Diff completo old→new de la entidad modificada.
* Propagación de `correlation_id` desde el request que lo causó.
* El helper que llama todo command handler, dentro de la transacción del comando.
* Endpoints de lectura para el rastro del propio tenant y para Softcrum Ops.
* Retención por plan, con el mínimo legal aplicando de forma independiente.

## Fuera de alcance *(normativo)*

* Logging de aplicación y trazas — eso es `packages/observability`, otra preocupación con otra
  retención y otro destino.
* Alertas sobre patrones de auditoría — una feature de seguridad posterior.
* Inmutabilidad por criptografía. Append-only más grants restringidos es la postura de F1; una
  cadena de hashes es un endurecimiento posible más adelante.

## Comportamiento *(normativo)*

1. Todo comando escribe exactamente una fila de auditoría **dentro de su propia transacción**. Si el
   comando revierte, la fila revierte con él — una entrada de auditoría de algo que no ocurrió es
   peor que ninguna.
2. El actor está **siempre tipado y siempre presente**. `system` es un actor válido; desconocido no.
3. El diff es `old` → `new` sobre los campos cambiados, completo (DEC-C2). Los valores sensibles se
   registran **enmascarados**: el registro prueba que el cambio ocurrió sin volverse una segunda
   copia del documento.
4. `correlation_id` es obligatorio y se hereda del request de origen, para que una fila de auditoría
   se una con los eventos, jobs y envíos que causó.
5. La tabla es **append-only**. Sin camino de update, sin camino de delete, y el rol de aplicación no
   tiene grant para ninguno.
6. Las lecturas que exponen datos sensibles escriben su propia fila — leer un documento completo es
   un acto auditable (DEC-A4).
7. Particionada mensualmente sobre `occurred_at`; toda consulta lleva el predicado de partición.
8. La retención sigue el tier del plan, y el **mínimo legal aplica de forma independiente y gana**
   cuando es mayor.
9. Un tenant puede leer su propio rastro, incluida la suplantación de operador ejercida sobre él.
   Ocultarlo anularía el propósito de registrarlo.

## Datos *(normativo)*

| Tabla            | Invariantes clave                                                                                                                                                                                                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `core.audit_log` | append-only; `tenant_id`, `cell_id`; `actor_type`, `actor_id`; `entity_type`, `entity_id`; `action`; `diff` JSONB con valores sensibles enmascarados; `correlation_id`; `occurred_at`; partición RANGE mensual; índice (`tenant_id`, `entity_type`, `entity_id`, `occurred_at DESC`) |

## API *(normativo)*

| Endpoint                 | Clase      | Permiso               | Presupuesto                                    |
| ------------------------ | ---------- | --------------------- | ---------------------------------------------- |
| `GET /v1/core/audit-log` | Management | `core.audit_log.read` | p95 \<1 s, cursor, rango de tiempo obligatorio |

El rango de tiempo es **obligatorio**, no con valor por defecto: es la clave de partición, y una
consulta de auditoría sin acotar es un scan completo de la tabla más grande del schema.

## Eventos *(normativo)*

Ninguno. El registro es consumidor de todo y productor de nada.

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

1. Un comando que revierte no deja fila de auditoría.
2. Todo command handler del código escribe una fila — impuesto por una regla de lint, no por
   revisión.
3. El diff de un update contiene exactamente los campos cambiados, con valores viejo y nuevo.
4. Un documento de identidad que aparezca en un diff está enmascarado.
5. Leer un documento completo produce una fila que nombra al actor y al contacto.
6. Una consulta de auditoría sin rango de tiempo se rechaza con un error tipado.
7. Los intentos de update y delete fallan a nivel de grant de base de datos, no solo en la
   aplicación.
8. **Negativo:** no existe fila de auditoría con actor ausente o sin tipar.

## Ejecución

Un solo slice, comando síncrono. Schema y particiones en TS-001; el helper en `packages/core` para
que todo módulo escriba la misma forma.

## Preguntas abiertas

| # | Pregunta                                                                                                  | Decide           | Para                            |
| - | --------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------- |
| 1 | ¿El mínimo legal de retención son 5 años (por cercanía tributaria) o menos para entidades no financieras? | Daniel + abogado | antes del lanzamiento comercial |
| 2 | ¿Encadenamos hashes ahora, o aceptamos append-only más grants para F1?                                    | 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.*
