Estructura del método + fundación de datos Sprint 1 (pre-ejecución)

This commit is contained in:
markosbenitez
2026-06-02 07:21:49 -03:00
parent ef9e4bf998
commit e116e59ee9
11 changed files with 242 additions and 1 deletions

View File

@@ -0,0 +1,110 @@
# ADR-001 — Modelo de privacidad y gobierno de datos
- **Estado:** Aceptado
- **Fecha:** (completar con fecha de commit)
- **Decisores:** Director del proyecto + asistente de dirección técnica
- **Contexto legal:** Paraguay, Ley N° 7593/2025 de Protección de Datos Personales
---
## Contexto
El producto corre la misma encuesta en N empresas con **dos propósitos en tensión**: operativo
(dashboard por empresa para decidir acciones) y macro (estudio país de la "economía de la
discapacidad"). Se tratan **datos sensibles** (salud/discapacidad). Resolver mal esta tensión
significa o filtrar datos confidenciales entre empresas, o perder el análisis macro que es el
diferencial.
**Marco legal aplicable:** la Ley N° 7593/2025 fue promulgada el 27/11/2025, está alineada con el
RGPD europeo, crea la Agencia Nacional de Protección de Datos Personales (ANPDP) con régimen
sancionatorio, y tiene vacatio legis de 24 meses (exigibilidad plena ~nov 2027). Reconoce categoría
especial para datos sensibles (incluye salud) y derechos de acceso, rectificación, oposición,
supresión, portabilidad y revocación del consentimiento. *No es asesoría legal; validar con
especialista antes de producción.*
## Decisión
Adoptamos un modelo de **privacy by design** con los siguientes componentes cerrados:
### 1. Anonimato como propiedad por-encuesta
El anonimato no es global de la plataforma: cada encuesta lo declara en su configuración. El wizard
de creación lo togglea y eso propaga a cómo se guarda cada respuesta.
### 2. Dos ejes de clasificación, ortogonales
- **Sensibilidad legal** (`comun` | `sensible` | `anonimizado`) → alimenta el **consentimiento**.
- **Seguridad** (`publico` | `interno` | `confidencial`) → alimenta **RLS y cifrado**.
"Confidencial" es seguridad, NO una categoría legal. Una encuesta puede ser confidencial en storage
y no-sensible legalmente, o al revés.
### 3. La herramienta constriñe, no solo permite
La clasificación no es pura auto-declaración del creador. Ciertos tipos de pregunta
(salud/discapacidad) **se auto-marcan sensibles** y elevan la encuesta, sin importar lo que elija el
creador. El wizard guía y bloquea por defecto hacia la clasificación correcta. (Operacionaliza los
principios de minimización y responsabilidad proactiva de la ley.)
### 4. El titular del consentimiento es el respondente, no la empresa
La empresa autoriza el despliegue; el individuo consiente el tratamiento de su propio dato sensible.
Ante divergencia entre un documento firmado por la empresa y la elección granular del titular en la
app, **gana siempre la elección del titular**.
### 5. Consentimiento granular, por capas y versionado
Checkboxes separados por propósito (operativo / macro N1 / macro N2...), no un único "acepto todo".
Cada respuesta guarda un registro inmutable con timestamp contra la versión vigente del texto. (Ver
ADR-003.)
### 6. Rol de responsable parametrizable (`controller_role`), con cascada
- **Modo A** (nosotros = responsable): macro a todos los niveles consentidos por el titular.
- **Modo B** (empresa = responsable, nosotros = encargado): macro **solo** vía Nivel 1 anonimizado
irreversible. Usar dato de Modo B para Nivel 2/3 sería ejercer de responsable encubierto → ilícito.
> **Regla limpia:** Modo A → macro a todos los niveles consentidos. Modo B → macro solo vía Nivel 1
> anonimizado. El wizard debe **mostrar la consecuencia** al elegir el modo, no ofrecerlo como
> dropdown plano.
### 7. Anónimo a nivel respondente, conservando atribución de empresa (las 4 de v1)
- **Sí** se conserva `company_id` + cuasi-identificadores (edad en bucket, sector, zona) → habilita
Nivel 2 (empresa A vs el resto) y comparación por segmento.
- **No** se recolecta `respondent_id`. El campo queda **reservado** en el esquema para encuestas
futuras, pero para estos 4 estudios el dato queda permanentemente en Nivel 1/2.
### 8. Distinción anónimo vs pseudónimo (textual, para no re-confundir)
La **comparación por segmento** ("25 / sector X / zona A" vs "zona B") usa los cuasi-identificadores
de cada fila y **no requiere** `respondent_id`. La clave pseudónima solo sirve para vincular varias
respuestas a la misma persona desconocida (longitudinal, re-contacto, dedup avanzado) — nada de eso
es caso de v1. La dedup simple se hace con `ip_hash`.
### 9. Umbral de k-anonimato mandatorio en visualización
Anónimo en storage **no** es a prueba de re-identificación: un n=1 por cruce de cuasi-identificadores
es identificable. Toda visualización compartida (incluido el dashboard de la empresa) suprime celdas
con n < umbral (default 5). La comparación individuo-vs-individuo está prohibida en dashboards
compartidos; la inspección fila-por-fila es solo análisis interno (Modo A).
### 10. Texto libre fuera del store analítico crudo
Las preguntas abiertas no se exponen crudas a reportes (vector de re-identificación). Se categorizan
antes (a futuro, vía LLM — está en BACKLOG).
## Alcance de esta etapa (v1)
Solo **Modo A**, solo encuestas **anónimas**, **sin Nivel 3 atribuible**. El esquema soporta más
(controller_role, respondent_id) pero v1 no lo opera. Principio: el esquema guarda la opción, estos
datos puntuales no la usan.
## Alternativas consideradas
- **Macro 100% anonimizado siempre (solo Nivel 1):** más simple, pero mata el benchmark sectorial que
el director quiere a futuro. Rechazado por cerrar opción de bajo costo.
- **Pseudonimización con `respondent_id` desde v1:** habilita longitudinal/re-contacto pero agrega
carga legal (es dato personal) sin caso de uso inmediato. Rechazado para v1; campo reservado.
- **Nivel 3 atribuible desde v1:** máximo poder analítico, pero requiere acuerdo de tratamiento por
empresa + consentimiento explícito + vía de re-identificación. Diferido hasta tener feedback real.
## Consecuencias
- (+) Cumplimiento alineado a Ley 7593 desde el diseño; soberanía; protección del empleado frente a
su empleador.
- (+) Mantiene viva la opción de Nivel 2 sin recolectar datos personales adicionales.
- () El dato de los 4 estudios no podrá usarse jamás para análisis atribuible ni longitudinal
individual (decisión consciente).
- () El umbral de k-anonimato puede dejar segmentos chicos sin reporte ("n insuficiente") — es el
costo correcto de la privacidad.
## Pendiente
Validación del flujo de consentimiento y del lenguaje legal con asesor paraguayo antes de producción.

View File

@@ -0,0 +1,52 @@
# ADR-002 — Stack tecnológico y residencia de datos
- **Estado:** Aceptado (stack) · **Abierto** (residencia de datos)
- **Fecha:** (completar con fecha de commit)
---
## Contexto
Se necesita un stack moderno, con experiencia móvil fluida, reportes potentes, soberanía de datos y
sin lock-in caro. El volumen no justifica una suite SaaS profesional. Se tratan datos sensibles.
## Decisión (stack)
| Capa | Tecnología | Razón |
|---|---|---|
| Frontend | Next.js (App Router) | SSR/SSG, ecosistema maduro, buena DX, móvil sólido |
| Form engine | React + react-hook-form + zod | Validación tipada, flexible, sin vendor lock-in |
| Backend / datos | Supabase (Postgres + Auth + RLS + Realtime) | Postgres real para reportes SQL, RLS nativo multi-tenant, self-host posible |
| Auth | Supabase Auth | Anónimo / OAuth / magic link según encuesta |
| Reportes/gráficos | ECharts (preferido) o Recharts | Gratis, potente, tipos de gráfico variados (heatmap, polar, pirámide), no-SaaS |
| Deploy app | Vercel | Rápido para arrancar, migrable |
Supabase es la pieza clave: Postgres real habilita los reportes cruzados (filtrar respuesta A por
respuesta B), RLS da el aislamiento multi-tenant que ADR-001 exige, y el modo self-host (Docker)
preserva la opción de soberanía total.
## Decisión ABIERTA — residencia de los datos
Supabase Cloud aloja datos en regiones US/EU por defecto. Para un producto cuyo requisito explícito
es **soberanía** y que trata **datos sensibles de ciudadanos paraguayos**, la ubicación física del
dato no es neutral:
- La Ley 7593/2025 tiene reglas de **transferencia internacional** (requiere nivel de protección
adecuado o garantías apropiadas en el país destinatario).
- "Soberanía total" sugiere self-host (Supabase en infraestructura propia/regional).
**Opciones:**
1. **Supabase Cloud** (US/EU) — más rápido de arrancar, menos operación, pero transferencia
internacional a evaluar legalmente y tensión con "soberanía".
2. **Supabase self-host** (Docker, infra propia o proveedor regional) — soberanía real, más operación.
3. **Híbrido:** arrancar en Cloud para el POC interno, migrar a self-host antes de producción con
datos reales de ciudadanos.
**No se cierra todavía.** Requiere: (a) decisión del director sobre apetito de operación, (b) input
legal sobre transferencia internacional bajo Ley 7593. **Bloqueante para producción, no para v1
interno** si v1 corre con datos de prueba/simulados.
## Consecuencias
- (+) Stack migrable; Supabase no es lock-in si se self-hostea.
- () Mantener la opción self-host implica disciplina (no usar features Cloud-only sin registrarlo).
- Registrar en este ADR cuándo y cómo se resuelve la residencia.

View File

@@ -0,0 +1,54 @@
# ADR-003 — Consentimiento granular, versionado e inmutable
- **Estado:** Aceptado
- **Fecha:** (completar con fecha de commit)
---
## Contexto
Bajo lógica RGPD / Ley 7593, el consentimiento para datos sensibles debe ser específico, informado y
**demostrable**: hay que poder probar qué consintió cada persona, cuándo, y contra qué texto. Además,
el director planteó dos caminos de consentimiento que pueden coexistir y divergir: documentos
firmados (empresa/encuestado) y checkboxes in-app.
## Decisión
### 1. El consentimiento es evidencia, no solo UI
Mostrar el checkbox no alcanza. Cada respuesta guarda un **registro de consentimiento** con: versión
del texto, scopes efectivamente aceptados, timestamp. Es **inmutable** (solo INSERT/SELECT; sin
UPDATE/DELETE).
### 2. Versionado del texto
El texto de consentimiento vive en `consent_versions`. Si cambia el wording en el futuro, las
respuestas viejas **conservan su versión vieja**. Nunca se reescribe retroactivamente lo que alguien
aceptó.
### 3. Granular por capas
Scopes separados por propósito, aceptables independientemente:
`operativo` (dashboard de su empresa), `macro_n1` (agregados anónimos del estudio país), y a futuro
`macro_n2` / `macro_n3`. **v1 ofrece solo `operativo` + `macro_n1`** (las encuestas son anónimas y
Modo A sin Nivel 3). Un titular puede aceptar operativo y rechazar macro.
### 4. La elección del titular gana sobre la firma de la empresa
Si la empresa firmó "todos los datos van al macro" pero el empleado destildó macro en la app, la
respuesta NO entra al macro. El registro in-app del titular es la fuente de verdad sobre su propio dato.
### 5. Honestidad sobre derechos en encuestas anónimas
En encuesta anónima:
- No se puede hacer firmar al encuestado nada identificable (destruiría el anonimato) → el registro
in-app es el **único** registro de consentimiento posible; no se complementa con firma del titular.
- No se puede **revocar** después (no se puede ubicar la respuesta). El texto debe declararlo
explícitamente **antes** de responder: "esta encuesta es anónima; no vas a poder revocar luego".
- El wizard, al togglear anonimato, cambia qué derechos se pueden ofrecer y ajusta el texto.
## Consecuencias
- (+) Demostrabilidad ante la ANPDP; trazabilidad por versión.
- (+) Respeta la autonomía del titular sobre su dato sensible.
- () Un mismo dato puede estar en el dashboard de la empresa pero excluido del macro (lógica de
reportes debe respetar `granted_scopes` por fila — no es opcional).
- () Encuestas anónimas pierden el derecho de revocación; debe comunicarse con transparencia.
## Relación con otros ADR
Implementa el componente de consentimiento de ADR-001. El esquema está en `DATA_MODEL.md`
(`consent_versions`, `consent_records`).

View File

@@ -0,0 +1,56 @@
# ADR-004 — Capa de identidad: respondente anónimo vs staff con SSO
- **Estado:** Aceptado (frontera) · Diferido (integración Keycloak)
- **Fecha:** (completar con fecha de commit)
---
## Contexto
El equipo construye una familia de productos Inquality que se autentican contra un **Keycloak**
propio (proyecto en curso de otro equipo). Surge la pregunta de si esa capa de identidad aplica a
este producto. El producto tiene dos audiencias con necesidades opuestas (respondente anónimo vs
staff interno), por lo que la respuesta no es única.
## Decisión
### Frontera dura (no negociable)
- **El flujo del respondente NUNCA se ata a un IdP.** Las encuestas de v1 son anónimas: el
respondente no se loguea, ni con Keycloak ni con nada. Atarlo a SSO destruiría el anonimato (la
premisa de v1) y agregaría fricción que baja la tasa de respuesta. Lado respondente = `anon` key +
RLS + sin identidad. Para siempre, mientras la encuesta sea anónima.
- **El staff interno SÍ puede usar SSO.** El acceso del equipo a dashboards/administración —a través
de la familia de productos— es donde Keycloak aporta: SSO único, gestión de usuarios centralizada,
offboarding en un solo lugar (control de seguridad real para dato sensible), una sola fuente de
verdad de identidad. Y Keycloak es self-hosteable → alinea con la dirección Dokploy/soberanía.
### Integración técnica (cuando Keycloak madure)
Supabase soporta *third-party auth*: confía en JWT de un emisor externo si son firmados
asimétricamente (RS256/ES256), expuestos vía OIDC Discovery URL, con `kid` en el header. Keycloak
cumple. Dos caminos válidos:
1. Registrar Keycloak como emisor OIDC confiable (más libre en self-host).
2. Patrón universal: verificar el JWT de Keycloak en el edge e intercambiarlo por un JWT firmado por
Supabase. Funciona con cualquier emisor.
RLS lee los claims del token externo con `auth.jwt()` igual que con tokens de Supabase Auth.
### No bloquear v1
Keycloak es un proyecto en curso de otro equipo. **v1 no depende de él.** v1 casi no necesita auth
(encuestas anónimas + dashboard que presenta el staff). Estrategia:
1. Diseñar las policies de RLS para que lean **claims por nombre** (`org_id`, `role`), no helpers
exclusivos de Supabase Auth, para que el emisor sea intercambiable.
2. v1 corre con auth nativa de Supabase para los pocos usuarios internos (o sin login interno si
alcanza).
3. Cuando Keycloak esté listo: emite esos claims, se registra como emisor confiable, y RLS sigue
andando sin reescribir.
## Consecuencias
- (+) Identidad portable: el emisor del token se cambia por config, no por reescritura.
- (+) Encaja con la familia de productos y con la soberanía (Keycloak self-host).
- () Disciplina: nunca basar RLS en `user_metadata` (modificable por el usuario); usar claims
verificados del servidor (Keycloak los da).
- () Cuando se integre, alinear los claims de Keycloak (`org_id`, `role`) con lo que esperan las
policies — tarea de config, no de esquema.
## Relación con otros ADR
Vive sobre el patrón de backend de ADR-002 (Supabase como backend). La regla de keys de `CLAUDE.md`
(rule 9) sigue aplicando: `service_role` nunca al cliente, pase lo que pase con el IdP.