4 Commits

Author SHA1 Message Date
markosbenitez
a4287a122e docs(compliance): crear COMPLIANCE.md + registrar roadmap de auditabilidad y seguridad 2026-06-22 15:26:22 -03:00
markosbenitez
6db211b9aa docs(adr): registrar ADR-005 esquema declarativo + deuda Fase 2 en backlog 2026-06-22 15:19:37 -03:00
markosbenitez
d50c31f8f4 feat(data): agregar instance_id a answers para multi-instancia [4A]
Habilita respuestas múltiples por pregunta (mini-wizard de vínculos).
Nueva constraint UNIQUE(response_id, question_id, instance_id).
Aplicada en producción. Parte del Sprint 4 hacia v3.0.0.
2026-06-22 14:35:13 -03:00
markosbenitez
d6be2b5c6b docs: agregar CHANGELOG con versionado semántico
Establece la convención SemVer para el proyecto.
v2.1.0 documentada como versión estable actual.
v3.0.0 (wizard) marcada como en desarrollo.
2026-06-22 09:10:00 -03:00
7 changed files with 363 additions and 1 deletions

View File

@@ -77,11 +77,15 @@ Principios estructurales (de ADR-001):
- **Umbral de k-anonimato** obligatorio en toda visualización compartida (nunca mostrar celda con - **Umbral de k-anonimato** obligatorio en toda visualización compartida (nunca mostrar celda con
n < umbral). n < umbral).
## 6. ADRs iniciales ## 6. ADRs
Ver `docs/adr/` para los archivos completos.
- **ADR-001** — Modelo de privacidad y gobierno de datos *(el corazón del producto)*. - **ADR-001** — Modelo de privacidad y gobierno de datos *(el corazón del producto)*.
- **ADR-002** — Stack tecnológico y residencia de datos. - **ADR-002** — Stack tecnológico y residencia de datos.
- **ADR-003** — Consentimiento granular versionado e inmutable. - **ADR-003** — Consentimiento granular versionado e inmutable.
- **ADR-004** — Capa de identidad y autenticación (SSO Keycloak para staff; sin login para respondentes).
- **ADR-005** — Esquema declarativo de formularios y estrategia de dos fases (Sprint 4). La lógica del flujo vive en datos (JSONB), interpretada por un motor backend; el frontend wizard es un renderer sin reglas de negocio. Motor genérico diferido a Fase 2.
## 7. Riesgos ## 7. Riesgos

View File

@@ -7,6 +7,19 @@
## Seguridad ## Seguridad
### [SEC-002] Sprint de compliance & seguridad
- **Estado:** Diferido — pendiente de priorizar tras Sprint 4
- **Contexto:** COMPLIANCE.md documenta el estado actual de seguridad (v2.1.0) y el roadmap
de hardening. Antes de escalar a más empresas o datos reales, se requiere un sprint dedicado.
- **Ítems principales (ver COMPLIANCE.md secciones 5 y 6):**
- Registro de auditoría append-only (log inmutable de operaciones sensibles)
- Hashing SHA-256 de integridad de respuestas (detección de tampering sin blockchain)
- Cifrado en reposo a nivel de volumen/disco de la base de datos
- Roadmap de hardening por capas: aplicación, autenticación, datos, infraestructura,
observabilidad y proceso (revisión periódica de CVEs, pen testing)
- **Dependencia:** no bloquea Sprint 4; sí bloquea escalar a producción masiva.
- **Ver:** `COMPLIANCE.md` secciones 5 y 6.
### [SEC-001] Actualizar Next.js — CVE pendiente en 15.3.3 ### [SEC-001] Actualizar Next.js — CVE pendiente en 15.3.3
- **Estado:** Pendiente - **Estado:** Pendiente
- **Prioridad:** Alta - **Prioridad:** Alta
@@ -58,6 +71,17 @@
--- ---
### [BACKLOG-002] Fase 2 — Motor genérico de formularios
- **Estado:** Diferido post-deadline
- **Contexto:** En Sprint 4 se construye un intérprete del esquema declarativo específico
para el formulario de cuidadores (ADR-005, Fase 1). Una vez entregado ese caso, el
intérprete se extrae a un motor universal que interprete cualquier esquema definido en
`questions.conditional_rules` + campos de instanciación, sin hardcodear ningún formulario.
El formulario de cuidadores se vuelve el primer caso de prueba del motor sin reescribir
el contrato de datos.
- **Dependencia:** Fase 1 completada (Sprint 4 — wizard funcional).
- **Ver:** ADR-005 (`docs/adr/ADR-005-esquema-declarativo-formularios.md`).
- Dashboard / reportes (Sprint 3) - Dashboard / reportes (Sprint 3)
- Formulario de consentimiento + persistencia de respuestas (Etapa 2B) - Formulario de consentimiento + persistencia de respuestas (Etapa 2B)
- Builder visual de encuestas (post-validación del caso fijo) - Builder visual de encuestas (post-validación del caso fijo)

58
CHANGELOG.md Normal file
View File

@@ -0,0 +1,58 @@
# Changelog — Datos País sobre el Cuidado
Todas las versiones notables de este proyecto se documentan en este archivo.
El formato sigue [Keep a Changelog](https://keepachangelog.com/es/1.1.0/)
y el proyecto adhiere a [Versionado Semántico](https://semver.org/lang/es/).
---
## [No publicado] — Sprint 4: Formulario Wizard v3
En desarrollo en `main`. Cambio mayor: reescritura del formulario a wizard
pregunta-por-pantalla con instancias de vínculo familiar.
### Por agregar
- Wizard pregunta-por-pantalla para todo el formulario
- Instrumento v3: 10 secciones, mini-wizard anidado para vínculos familiares
- Columna `instance_id` en `answers` para datos multi-instancia
- Autocomplete de barrios desde dataset INE/DGEEC
- Subtítulos explicativos del "porqué" por sección
---
## [2.1.0] — 2026-06-16
Versión estable preservada en la rama `v2.1.0-stable`. Última versión del
formulario lineal antes del rediseño a wizard.
### Agregado
- Dashboard multitenante con selector de empresa (Todos / InQuality / Fundación / Demo)
- Flag `p_all` (UUID cero) para vista agregada de todos los surveys
- Vista de control `/admin/responses` con tabla plana, filtro por empresa,
colapsar/expandir por submission
- Dos encuestas piloto: InQuality y Fundación Solidaridad (organizaciones separadas)
- Método formalizado: `method/PATTERNS.md` y `method/ANTI_PATTERNS.md`
(patrones C.8, AP.10, AP.11)
### Cambiado
- Instrumento actualizado a v2.1 (feedback equipo médico): salto triple,
nueva pregunta 6.6, eliminación de "Prefiero no decir", 2.4 eliminada,
2.1 a multiple_choice
- Next.js actualizado a 15.3.8 (pin exacto)
### Seguridad
- Resuelto CVE-2025-66478 (+ advisory posterior CVE-2025-55183/55184/67779)
### Infraestructura
- Migración completa a Supabase self-hosted en Dokploy
- Backups GFS locales con retención de 3 días + domingos 14 días
---
## [2.0.0] — 2026-06 (anterior)
Primera versión en producción con Supabase self-hosted.
- Formulario lineal de 44 preguntas
- Dashboard con 11 widgets y k-anonimato (k≥5)
- Instrumento v2 (44 preguntas, 8 secciones)

141
COMPLIANCE.md Normal file
View File

@@ -0,0 +1,141 @@
# COMPLIANCE.md — Cumplimiento, seguridad y auditabilidad
> Documento vivo. Área: Compliance & Seguridad del producto "Datos País sobre el Cuidado".
> Audiencia: dirección técnica, auditores externos, equipo legal, clientes (resumen).
> Última actualización: 2026-06-20 · Versión del producto: 2.1.0 (prod) / 3.0.0 (desarrollo)
---
## 1. Principio rector de seguridad
**No existe un sistema "imposible de hackear". El objetivo es defensa en profundidad:**
encarecer progresivamente el ataque, minimizar la superficie expuesta, contener el daño
si una capa cae, y garantizar trazabilidad para detectar, auditar y reconstruir cualquier
incidente. Toda afirmación de inviolabilidad absoluta se considera, en sí misma, una
señal de inmadurez en seguridad.
---
## 2. Naturaleza de los datos y marco normativo
### 2.1 Clasificación de datos
- **Datos sensibles** (salud, discapacidad): preguntas marcadas `is_sensitive=true`
(Sección de prevalencia, condición propia y de familiares).
- **Cuasi-identificadores**: departamento, ciudad, sector, rango etario, género.
- **Dato confidencial restringido**: franja salarial (analyst-only, nunca en vistas de empresa).
- **NO se recogen identificadores directos**: sin nombre, documento, email ni teléfono
en la encuesta anónima. La encuesta es anónima por diseño.
### 2.2 Marco normativo aplicable (Paraguay)
- Ley N° 6534/2020 de Protección de Datos Personales Crediticios y normativa de datos
personales aplicable en Paraguay.
- Principios alineados a estándares internacionales (minimización, finalidad, consentimiento).
- **Pendiente de revisión legal formal** con el equipo de Fundación Solidaridad / asesoría legal.
### 2.3 Consentimiento
- Registrado en tablas `consent_records` + `consent_versions` con versión y timestamp.
- Doble propósito separado y autorizable independientemente: diagnóstico interno de empresa
(requerido) y estudio país (opcional).
- Por ser anónima, la encuesta no permite revocación post-envío (no hay forma de localizar
una respuesta individual) — comunicado explícitamente al participante en el consentimiento.
---
## 3. Anonimato y protección contra reidentificación
### 3.1 k-anonimato (k ≥ 5)
- Regla no negociable del sistema: ningún corte de datos con menos de 5 respuestas se muestra.
- Implementado en las RPCs del dashboard (`SECURITY DEFINER`), no en el frontend.
- Estados de supresión: `suppressed` con `n` real solo a nivel agregado.
### 3.2 Separación de datos sensibles
- Franja salarial (3.4 / 8.4 según versión): nunca expuesta en vistas de empresa
ni en `/admin/responses`.
- Política RLS `deny_sensitive` a nivel de base de datos.
- Vista de control `/admin/responses` excluye `is_sensitive=true` y `text_long` a nivel
de query, no de presentación (los valores no llegan al servidor).
---
## 4. Seguridad — estado actual (v2.1.0)
### 4.1 Implementado
- **Cifrado en tránsito**: TLS/SSL vía Kong + Traefik (Let's Encrypt) en `db.inqualityhq.com`
y `economia-cuidado.inqualityhq.com`.
- **Soberanía del dato**: Supabase self-hosted en infraestructura propia (VPS controlado),
no en nube de terceros. Decisión arquitectónica fundacional.
- **RLS (Row Level Security)**: políticas activas en tablas sensibles; el rol `anon` solo
puede insertar respuestas, no leer datos de otros.
- **Service role aislado**: la clave de servicio nunca llega al bundle del cliente
(verificado: vistas server-side, no Client Components con la key).
- **Gestión de vulnerabilidades**: CVE-2025-66478 resuelto (Next.js 15.3.8, pin exacto).
- **Backups**: GFS local (3 días diarios + domingos 14 días), restore probado.
- **Aislamiento de red**: Studio de administración solo accesible vía SSH tunnel, no expuesto.
### 4.2 Deuda de seguridad registrada (ver TECH_DEBT.md)
- **TECH-DEBT-006**: backup remoto S3 — sin réplica off-site, riesgo ante pérdida del VPS.
Bloqueante antes del primer dato real de empresa.
- **TECH-DEBT-008**: `/admin/responses` sin autenticación — mitigado por URL no publicada,
pero requiere gate de auth antes de exponer.
- **TECH-DEBT-009**: rotación de secrets Supabase post-CVE — pendiente.
---
## 5. Auditabilidad y trazabilidad (roadmap)
> Objetivo: ante una auditoría o un incidente, poder responder "quién accedió a qué,
> cuándo, y demostrar que un registro no fue alterado".
### 5.1 Pendiente de diseño e implementación (BACKLOG — sprint de compliance)
- **Registro de auditoría append-only**: log inmutable de operaciones sensibles
(accesos a datos sensibles, cambios de configuración, ejecución de migraciones).
- **Hashing de integridad (SHA-256)**: hash de los registros de respuesta para demostrar
no-alteración, encadenable para detección de tampering. Provee el beneficio probatorio
de inmutabilidad SIN los costos de blockchain.
- **Cifrado en reposo**: cifrado a nivel de volumen/disco de la base de datos.
- **Trazabilidad de cambios de datos**: tablas de historial con triggers, o aprovechamiento
del WAL de PostgreSQL para reconstrucción temporal.
### 5.2 Por qué NO blockchain (decisión registrada)
Blockchain aporta valor cuando se requiere registro inmutable verificable entre partes
que NO confían entre sí, sin autoridad central legítima. En este sistema la confianza
está legítimamente centralizada (InQuality es responsable y operador soberano del dato),
por lo que blockchain no resuelve ningún problema real que no esté mejor resuelto por
audit logs + hashing. Además, su inmutabilidad absoluta entra en CONFLICTO con el derecho
de rectificación y supresión exigido por la normativa de protección de datos. Se descarta
por inadecuación técnica y normativa, no por desconocimiento.
---
## 6. Roadmap de hardening de seguridad (a evaluar en sprint de compliance)
> Defensa en profundidad — capas a evaluar e implementar gradualmente.
- **Capa de aplicación**: validación y sanitización de inputs, protección contra inyección
(parametrización ya usada vía PostgREST/Supabase), rate limiting en endpoints públicos,
headers de seguridad (CSP, HSTS, X-Frame-Options).
- **Capa de autenticación**: gate de auth para vistas administrativas, rotación de secretos,
gestión de sesiones robusta cuando se introduzcan roles autenticados.
- **Capa de datos**: cifrado en reposo, RLS exhaustivo, principio de mínimo privilegio en roles.
- **Capa de infraestructura**: firewall, fail2ban, actualizaciones de SO, monitoreo de
intrusiones, segmentación de red.
- **Capa de observabilidad**: logs centralizados, alertas ante patrones anómalos,
registro de auditoría (sección 5).
- **Proceso**: revisión periódica de dependencias (`npm audit`), gestión de CVEs,
pruebas de penetración antes de escalar a producción masiva.
---
## 7. Responsabilidades
- **Responsable del dato**: InQuality (operador de infraestructura y autoridad del dato).
- **Aliado institucional**: Fundación Solidaridad.
- **Revisión legal**: pendiente de formalizar con asesoría legal especializada en
protección de datos en Paraguay.
---
## 8. Historial de revisiones de este documento
| Fecha | Versión producto | Cambio |
|---|---|---|
| 2026-06-20 | 2.1.0 / 3.0.0-dev | Creación inicial. Estado de seguridad v2.1.0, roadmap de auditabilidad, decisión sobre blockchain. |

View File

@@ -106,9 +106,20 @@ id uuid pk
response_id uuid fk -> responses(id) response_id uuid fk -> responses(id)
question_id uuid fk -> questions(id) question_id uuid fk -> questions(id)
value jsonb not null -- texto: "x" | matriz: {"rows":{...}} | ranking: [3,1,2] | etc. 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 > 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). > 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 ### `consent_versions` — textos de consentimiento versionados
```sql ```sql

View File

@@ -0,0 +1,102 @@
# ADR-005 — Esquema declarativo de formularios y estrategia de dos fases
> Para agregar a `ARCHITECTURE.md` (o `docs/adr/`). Estado: **Aceptada**.
> Fecha: 2026-06-20 · Sprint 4 · Versión objetivo 3.0.0
---
## Contexto
El producto es un **motor de formularios/flujos de encuesta**. La aplicación
actual (encuesta "Mapeo de Cuidadores") es su primer caso de uso real, pero la
arquitectura debe servir al producto, no solo a este formulario.
El rediseño a wizard v3 introduce lógica de flujo significativamente más compleja
que v2: recorridos ramificados (cuidador completo / solo niño / sin vínculo),
mini-wizard de instancias de vínculo familiar (preguntas que se repiten N veces,
una por familiar), y saltos condicionales de sección.
Existe una restricción de negocio: dos clientes inminentes en 10-20 días requieren
la encuesta funcionando para validación.
El esquema actual de `questions` ya posee una base declarativa:
- `conditional_rules` JSONB con operadores `eq` / `eq_any` y acción `show`
- Auto-target implícito (la regla afecta a la pregunta que la porta)
- Tipos de pregunta extensibles vía CHECK constraint
## Problema
¿Dónde reside la lógica del flujo del formulario? Tres anti-patrones a evitar:
1. **Lógica en el presentador** (frontend con reglas hardcodeadas) — impide
reutilización, genera bugs de sincronización de estado, no escala al producto.
2. **Lógica dispersa en columnas sueltas** de la base — deuda técnica; no soporta
evolución a flujos más complejos sin migrar el esquema entero.
3. **Sobre-ingeniería prematura** — construir un motor genérico universal para
casos hipotéticos antes de conocerlos, retrasando el deadline real.
## Decisión
**La lógica del flujo vive en una capa de definición declarativa (datos),
interpretada por un motor en el backend, y renderizada por un frontend sin reglas
de negocio.**
Se adopta una **estrategia de dos fases**:
### Fase 1 (Sprint 4 — para el deadline)
- El formulario de cuidadores se define como **esquema declarativo persistido**,
extendiendo el `conditional_rules` JSONB existente y agregando campos
declarativos de instanciación a `questions`.
- Se construye un motor de resolución de flujo **para este formulario** (interpreta
el esquema; calcula qué pantalla sigue desde esquema + estado).
- El frontend wizard es un **renderer** del esquema, sin reglas de negocio.
- NO se construye el intérprete genérico universal todavía.
### Fase 2 (sprints posteriores — post-deadline, registrada en BACKLOG)
- Se extrae el intérprete a un **motor genérico de formularios**.
- El formulario de cuidadores, ya definido como esquema, se vuelve el primer caso
de prueba del motor — sin reescribir el contrato.
### Diseño técnico del esquema (Fase 1)
Se extiende `questions` en lugar de crear una tabla de definición separada:
- `conditional_rules` extendido con nuevos operadores (`not_in`, acción `hide`)
para expresar saltos como "Ninguno → salta a sección final".
- Nuevos campos declarativos para instanciación:
- `instance_generator` — marca la pregunta que genera instancias (ej. 4.1)
- `instance_of` — código del generador del que depende una pregunta (ej. 4.2 → "4.1")
- Representación de secciones con nombre y descripción (el "porqué").
## Alternativas consideradas
1. **Lógica en el frontend (rechazada):** rápida para este formulario, pero viola
el principio de producto, no escala, y reproduce los bugs de sincronización ya
vistos en el mockup.
2. **Columnas sueltas tipo `is_instanced` (rechazada):** resuelve hoy, genera deuda;
no soporta flujos más complejos sin migración de esquema.
3. **Motor genérico universal ahora (rechazada):** arquitectónicamente deseable como
fin, pero sobre-ingeniería antes del deadline y con un solo caso de uso conocido
(diseñar en el vacío). Se difiere a Fase 2 cuando haya más casos que informen el
diseño abstracto.
4. **Tabla de definición de flujo separada (diferida):** posible mejor forma para el
motor genérico, pero para un solo formulario es sobre-ingeniería. Se reevalúa en
Fase 2.
## Consecuencias
**Positivas:**
- Entrega la encuesta en plazo sin generar retrabajo estructural.
- La lógica en datos habilita guardado parcial, reanudación, validación consistente
y progreso preciso — mejor UX, fuente única de verdad del estado.
- La Fase 2 es generalización del intérprete, no reescritura del contrato.
- El esquema declarativo es versionable y auditable.
**Negativas / deuda intencional:**
- Fase 1 es más trabajo que parchear el formulario actual.
- El motor de Fase 1 es específico a este formulario (no genérico) — deuda
registrada en BACKLOG, asumida conscientemente, no oculta.
- Extender `conditional_rules` puede requerir reevaluación cuando lleguen flujos
con lógica que el vocabulario actual de operadores no exprese.
## Cumplimiento (cómo se verifica)
- Revisión de código: el frontend wizard NO contiene reglas de flujo del negocio.
- El flujo es reproducible desde el esquema persistido.
- La deuda de Fase 2 figura en `BACKLOG.md`.

View File

@@ -0,0 +1,22 @@
-- Sprint 4 (v3.0.0) — instance_id para respuestas multi-instancia
-- Habilita el mini-wizard de vínculos familiares (una respuesta por familiar)
-- Nombre original de constraint verificado: answers_response_id_question_id_key
-- (inline UNIQUE en CREATE TABLE, migración 20260531000001_create_tables.sql, sin renombrado posterior)
-- 1. Agregar la columna con default para filas existentes
ALTER TABLE public.answers
ADD COLUMN IF NOT EXISTS instance_id text NOT NULL DEFAULT 'default';
-- 2. Eliminar la constraint única vieja
ALTER TABLE public.answers
DROP CONSTRAINT IF EXISTS answers_response_id_question_id_key;
-- 3. Crear la nueva constraint que incluye instance_id
ALTER TABLE public.answers
ADD CONSTRAINT answers_response_question_instance_key
UNIQUE (response_id, question_id, instance_id);
-- 4. Índice para consultas por instancia (análisis de vínculos)
CREATE INDEX IF NOT EXISTS idx_answers_instance
ON public.answers (question_id, instance_id)
WHERE instance_id != 'default';