# DATA_MODEL.md — Contrato del modelo de datos

> Este es el **contrato** que Claude Code implementa como migraciones de Supabase + policies de RLS
> en la Etapa 1. El DDL de abajo es **ilustrativo del contrato** (columnas, tipos, intención), no la
> migración final: el agente escribe las migraciones reales, constraints, índices y RLS. No agregar
> tablas que no estén acá sin aprobación (Nivel 3).

Convención: snake_case, claves `uuid` (default `gen_random_uuid()`), timestamps `timestamptz`.
`UMBRAL_K` se referencia como constante de proyecto (default 5).

---

## Diagrama lógico

```
organizations 1──┐
                 │
surveys ─────────┤ (controller_role, is_anonymous, sensibilidad, seguridad)
   │             │
   ├─ questions ─┤ (type, is_sensitive auto, conditional_rules)
   │             │
responses ───────┘ (company_id, respondent_id NULLABLE, ip_hash, consent_record_id)
   │
   ├─ answers (value JSONB)
   │
consent_versions 1──< consent_records (snapshot inmutable por respuesta)
```

---

## Tablas

### `organizations` — empresas cliente
```sql
id            uuid pk
name          text not null
sector        text            -- para agrupar/benchmark sectorial (Nivel 2)
size_bucket   text            -- rango de tamaño, NO headcount exacto (minimización)
created_at    timestamptz default now()
```

### `surveys` — definición de cada estudio
```sql
id                    uuid pk
org_id                uuid fk -> organizations(id)   -- empresa para la que corre
title                 text not null
status                text default 'draft'           -- draft | live | closed
-- Eje 1: anonimato (propiedad técnica, irreversible una vez live)
is_anonymous          boolean not null                -- true en las 4 de v1
-- Eje 2a: sensibilidad legal (alimenta consentimiento)
data_sensitivity      text not null                   -- 'comun' | 'sensible' | 'anonimizado'
-- Eje 2b: seguridad (alimenta RLS/cifrado) — ORTOGONAL al anterior
security_class        text not null                   -- 'publico' | 'interno' | 'confidencial'
-- Rol de responsable
controller_role       text not null default 'A'       -- 'A' = nosotros | 'B' = empresa (no usado en v1)
-- Consentimiento vigente al publicar
consent_version_id    uuid fk -> consent_versions(id)
-- Marca / look & feel
brand_config          jsonb                           -- { logo_url, palette, ... }
k_threshold           int not null default 5          -- umbral de k-anonimato para esta encuesta
created_at            timestamptz default now()
published_at          timestamptz
```
> El agente NO calcula `data_sensitivity` a partir de texto libre del creador: ciertos `questions.type`
> (salud/discapacidad) **fuerzan** `is_sensitive = true` a nivel pregunta y elevan la encuesta a
> `sensible`. La herramienta constriñe, no solo permite (ver ADR-001).

### `questions`
```sql
id                 uuid pk
survey_id          uuid fk -> surveys(id)
code               text             -- 'B1','C1','H1'... (del mockup)
type               text not null    -- text_short|text_long|single_choice|multiple_choice|
                                     -- rating|nps|matrix|ranking|date|slider|yes_no|section
prompt             text not null
position           int not null
is_sensitive       boolean not null default false   -- auto-true para tipos de salud/discapacidad
max_select         int              -- p/ multiple_choice con límite (H1 = 3)
options            jsonb            -- [{value,label}, ...]
conditional_rules  jsonb            -- [{ if_question, op, value, action: show|hide|skip, target }]
required           boolean default false
```

### `responses` — una submission (anónima en v1)
```sql
id                 uuid pk
survey_id          uuid fk -> surveys(id)
company_id         uuid fk -> organizations(id)      -- SE CONSERVA (habilita Nivel 2)
respondent_id      uuid                               -- RESERVADO, NULL en encuestas anónimas
ip_hash            text                               -- SHA-256 + salt, dedup sin identidad
consent_record_id  uuid fk -> consent_records(id) not null
submitted_at       timestamptz default now()
-- cuasi-identificadores denormalizados para segmentación eficiente (también viven en answers)
qi_age_bucket      text                               -- rango, no edad exacta (minimización)
qi_sector          text
qi_zone            text
```
> Regla: si `surveys.is_anonymous = true`, el INSERT que intente setear `respondent_id` debe fallar
> (constraint/trigger). Es la barrera técnica de la regla no negociable #2 de `CLAUDE.md`.

> **Mapeo v3 (Etapa 4G, confirmado):** `qi_zone` ← `2.1` (departamento), `qi_age_bucket` ← `2.4`
> (rango de edad). `qi_sector` ← `2.6` (nivel organizacional) — v3 no tiene una pregunta
> equivalente a "área/gerencia" como v2 (`A4`); `2.6` es el cuasi-identificador organizacional
> más cercano disponible en el instrumento actual, no un mapeo 1:1 con el campo de v2.
> Los cuasi-identificadores se guardan en **bucket** (rango etario, no fecha de nacimiento) para
> minimizar re-identificación.

### `answers`
```sql
id            uuid pk
response_id   uuid fk -> responses(id)
question_id   uuid fk -> questions(id)
value         jsonb not null     -- texto: "x" | matriz: {"rows":{...}} | ranking: [3,1,2] | etc.
instance_id   text not null default 'default'
              -- 'default'    → preguntas normales (la gran mayoría)
              -- 'familiar_1', 'familiar_2', ... → instancias del mini-wizard de vínculos (4A, Sprint 4)
UNIQUE (response_id, question_id, instance_id)   -- constraint: answers_response_question_instance_key
```
> El texto libre (`text_long`) **no** se expone al store analítico crudo: la capa de reportes lee
> categorías derivadas, no `value` crudo de preguntas abiertas (regla no negociable #4).
>
> **Patrón `instance_id` (v3.0.0):** la pregunta 4.1 ("¿Tenés familiar con discapacidad/adulto
> mayor a cargo?") dispara el mini-wizard. Por cada familiar declarado se genera un conjunto de
> respuestas para las preguntas 4.2, 4.3, etc., cada una con su propio `instance_id`. Las RPCs
> de dashboard que agregan datos de vínculo operan sobre filas donde `instance_id != 'default'`.
> Las RPCs existentes de v2 no se modifican: siguen usando `instance_id = 'default'` implícitamente
> (las preguntas de vínculo no estaban en el instrumento v2).

### `consent_versions` — textos de consentimiento versionados
```sql
id            uuid pk
version       text not null         -- 'v1', 'v2'...
body          text not null         -- texto completo presentado al titular
scopes        jsonb not null        -- catálogo de scopes que este texto ofrece
                                     -- ej: ["operativo","macro_n1"] (v1 NO incluye macro_n3)
created_at    timestamptz default now()
```

### `consent_records` — qué consintió cada respuesta (INMUTABLE)
```sql
id                  uuid pk
consent_version_id  uuid fk -> consent_versions(id) not null
granted_scopes      jsonb not null     -- subconjunto efectivamente aceptado por el titular
                                        -- ej: {"operativo":true,"macro_n1":true}
granted_at          timestamptz not null default now()
-- inmutable: sin UPDATE/DELETE; se inserta una vez junto con la response
```
> En encuesta anónima NO hay revocación posible (no se puede ubicar la respuesta). El texto de
> consentimiento debe declararlo explícitamente al titular antes de responder (ADR-003).

---

## Intención de RLS (Claude Code escribe las policies)

- Una `organization` solo puede leer **agregados** de sus propias `responses`, nunca filas crudas, y
  solo por encima de `k_threshold`. No existe policy que devuelva una fila individual a una empresa.
- El rol de **análisis macro** (interno, responsable = nosotros) puede leer filas para análisis
  propio, pero la **capa de reportes/exportación** aplica el umbral de k-anonimato y excluye texto
  libre crudo antes de exponer cualquier cosa.
- `consent_records` y `consent_versions`: solo INSERT y SELECT. Sin UPDATE/DELETE (inmutabilidad).

---

## Vistas/funciones helper esperadas (Etapa 1)

- `agg_segment(survey_id, dimensions[])` → agregación que **suprime** automáticamente celdas con
  `count < k_threshold` (devuelve "n insuficiente" en lugar del valor).
- Un trigger/constraint que rechaza `respondent_id` no nulo cuando `is_anonymous = true`.

---

## Decisiones ratificadas en ejecución (Sprint 1)
- **`options` en preguntas `rating` (F3, G4):** se guarda como objeto
  `{"min":1,"max":5,"min_label":"...","max_label":"..."}`, NO como array. El frontend (Sprint 2)
  debe manejar dos formas de `options`: array para `single_choice`/`multiple_choice`, objeto para
  `rating`.
- **`required = false` en todas las preguntas seeded:** la obligatoriedad real se define en Sprint 2
  (validación del formulario). Nota: B1 estaba marcada obligatoria en el mockup; revisar al construir
  el formulario.
- **Policies de `org_admin` presentes pero inertes en v1:** las policies RLS para `org_admin`
  (acceso multi-tenant por empresa) existen en migración 003, pero v1 NO tiene login de cliente
  ("nosotros presentamos a cada empresa"). El acceso real lo hace el analyst vía `service_role`. El
  camino `org_admin` queda reservado para un futuro de auto-servicio, no operado en v1.
