Compare commits

10 Commits

Author SHA1 Message Date
markosbenitez
424071b97c estado de proyecto updated 2026-06-23 15:01:08 -03:00
markosbenitez
eba93db452 feat(engine): motor de resolución de flujo declarativo [4C] 2026-06-23 07:21:35 -03:00
markosbenitez
ed524af16b feat(engine): motor de resolución de flujo declarativo [4C]
Módulo puro que interpreta el esquema v3 y resuelve pantallas.
5 operadores (eq, eq_any, not_in, contains, eq_sole), acciones show/hide
(hide = veto absoluto). Instancias con ID estable por posición de opción,
secciones-pantalla. Navegación por ID estable. 16/16 tests Node en verde.
No reemplaza aún lib/conditional.ts de v2 (lo hace 4D al migrar el componente).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 07:21:03 -03:00
markosbenitez
5987502664 feat(schema): esquema declarativo + seed v3 del formulario [4B]
Extiende questions con instance_generator/instance_of/hint.
Instrumento v3 completo (10 secciones, 50 preguntas) como datos declarativos.
Operadores de flujo para multiple_choice: contains, eq_sole.
Flujo del formulario en el esquema, no en presentación (ADR-005).
Seed solo reemplaza catálogo; no toca evidencia de consentimiento (ADR-003).
2026-06-22 21:05:11 -03:00
markosbenitez
aa893368cd feat(schema): esquema declarativo + seed v3 del formulario [4B]
Extiende questions con instance_generator/instance_of/hint.
Instrumento v3 completo (10 secciones, 50 preguntas) como datos declarativos.
Operadores de flujo para multiple_choice: contains, eq_sole.
Flujo del formulario en el esquema, no en presentación (ADR-005).
Seed solo reemplaza catálogo; no toca evidencia de consentimiento (ADR-003).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 21:03:01 -03:00
markosbenitez
7e4991f960 feat(schema): esquema declarativo + seed v3 del formulario [4B]
Extiende questions con instance_generator/instance_of/hint y documenta
los operadores contains/eq_sole en conditional_rules (ADR-005).

Reescribe seed.sql con el instrumento v3.0.0 completo: 10 secciones,
40 preguntas (2 consentimiento + 38), contenido 100% desde
Formulario_Mapeo_Corregido.xlsx (versión autoritativa). Flujo
declarativo en datos, no en presentación: 3.1='No' oculta 3.2-3.5;
"Ninguna" en 4.1 salta a S10; "solo niño sin discapacidad" deja
visibles 5.3, 8.4, 9.1, 9.4 y oculta el resto (decisión confirmada
sobre el Excel corregido).

SEED_SPEC.md reescrito como contrato v3.0.0.

No ejecutado en producción — solo archivos de migración y seed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 17:21:11 -03:00
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
21 changed files with 3022 additions and 1977 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

@@ -1,152 +1,209 @@
# SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v2.1 # SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v3.0.0
> Versión 2.1 — incorpora feedback del equipo médico, Etapa 3I2 (Jun 2026). Reemplaza la v2. > Versión 3.0.0 — Sprint 4, Etapa 4B (Jun 2026). Reemplaza la v2.1. Reescritura completa del
> 44 preguntas (se elimina 2.4, se agrega 6.6) · 8 secciones · Códigos únicos corregidos. > instrumento: 10 secciones, 40 preguntas (2 consentimiento + 38 de relevamiento), esquema
> declarativo de flujo (ADR-005). Seed LIMPIO — no migra datos de v2.1.
>
> Fuente de contenido AUTORITATIVA: `Formulario_Mapeo_Corregido.xlsx` (versión corregida,
> en rojo = cambios aplicados). El mockup `wizard_formulario_mapeo_v3.html` define estructura
> de secciones y mecánica del mini-wizard (4.1→4.2/4.3); donde el Excel difiere en el flujo
> condicional de "solo niño sin discapacidad", gana el Excel (decisión de la directora, ver
> abajo "Flujo condicional — recorrido soloNino").
>
> Claude Code reescribe `seed.sql` a partir de ESTE contrato. > Claude Code reescribe `seed.sql` a partir de ESTE contrato.
## Convenciones ## Convenciones
- Tipos: radio→`single_choice`, checkbox→`multiple_choice`, escala 1-5→`rating`,
texto libre largo→`text_long`, texto corto→`text_short`.
- **`conditional_rules` en la pregunta DEPENDIENTE** (cada pregunta declara su propia condición).
- Los 3 triggers (1.1, 1.6, 1.13) son `single_choice` de 2 opciones (Sí · No); solo **"Sí"** dispara
el bloque correspondiente.
- `is_sensitive=true` en preguntas de salud/discapacidad/diagnóstico (marcadas ✅).
- `3.4` (salary) — `is_sensitive=true` + analyst-only (ADR-001 addendum): nunca en vistas org_admin.
- RE-INDEX: las preguntas marcadas son candidatas a índice compuesto (metodología TBD Sprint 3).
- En preguntas `multiple_choice` el formulario muestra el subtítulo "Podés marcar más de una opción".
- "Prefiero no decir" eliminada de todas las preguntas (Cambio 1, Etapa 3I2).
## Estructura condicional (4 bloques) - Tipos: radio→`single_choice`, checkbox→`multiple_choice`, texto libre corto→`text_short`,
- **cond-disc-propia** — 1.1='Sí' → muestra 1.2, 1.3, 1.4, 1.5 texto libre largo→`text_long`, sí/no→`yes_no`.
- **cond-familiar** — 1.6='Sí' → muestra 1.7, 1.8, 1.9, 1.10, 1.11, 1.12 - Secciones persisten como filas `questions.type='section'`: `prompt`=nombre de sección,
- **cond-adulto-mayor** — 1.13='Sí' → muestra 1.14, 1.15, 1.16 `hint`=descripción/"porqué" (Opción C, ver ADR-005 §4.2).
- **salto-triple** — si 1.1='No' Y 1.6='No' Y 1.13='No' (es decir, ninguno de los tres dio - `conditional_rules` vive en la pregunta/sección DEPENDIENTE (auto-target implícito, sin
'Sí'), se ocultan completas las secciones 2, 3, 4, 5 y las preguntas 6.1, 6.2, 6.3. El campo `target`). Vocabulario completo en `supabase/migrations/20260620000002_form_schema_v3.sql`:
formulario salta directo a 6.4. Implementado como `conditional_rules` operadores `eq`, `eq_any`, `not_in`, `contains`, `eq_sole`; acciones `show`, `hide`.
`{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}` en cada - `is_sensitive=true` en preguntas de salud/discapacidad/diagnóstico y en 8.4 (franja salarial,
pregunta del bloque (equivalente: se muestra si AL MENOS uno de los tres es 'Sí'). CONFIDENCIAL — analyst-only, nunca en vistas de empresa, igual que 3.4 en v2.1).
- Sección A, 6.4-6.6 y Sección 7 (cierre): **top-level, siempre visibles.** - En preguntas `multiple_choice` el `hint` suele ser "Podés marcar más de una opción".
- Mini-wizard de instanciación (ADR-005): 4.1 es `instance_generator=true`; 4.2 y 4.3 tienen
`instance_of='4.1'` y se repiten una vez por cada vínculo familiar (opciones 27) marcado en 4.1.
Las respuestas de cada instancia se guardan en `answers.instance_id` (`'familiar_1'`, `'familiar_2'`...).
- Geo: 2.1 (departamento, 18 opciones) y 2.2 (ciudad, 219 opciones filtradas por departamento en
el renderer) son cuasi-identificadores. 2.3 (barrio) es `text_short` de texto libre, opcional —
NO entra al store analítico crudo sin categorizar (regla 4 CLAUDE.md); se usa solo como apoyo
cualitativo, no agregable.
## Flujo condicional — las 3 reglas principales
1. **`r_show_si_s3_1`** — 3.1='Sí' → muestra 3.2, 3.3, 3.4, 3.5 (discapacidad propia).
Si 3.1='No', toda la sub-rama queda oculta.
2. **`r_hide_ning`** — 4.1 contiene "Ninguna de las anteriores / No tengo vinculación de cuidado"
→ oculta S5, S6, S7, S8(header), 8.4, S9(header), 9.1, 9.4. El formulario salta directo a S10.
3. **`r_hide_ning_sn`** (Ninguna O soloNino) — además de la condición anterior, también oculta
cuando 4.1 tiene como ÚNICO elemento seleccionado "Hijo menor sin discapacidad" (`eq_sole`).
Oculta: 5.1, 5.2, 5.4, S6 completa, S7 completa, 8.1, 8.2, 8.3, 9.2, 9.3.
### Flujo condicional — recorrido "solo niño sin discapacidad" (soloNino)
Decisión de la directora (Etapa 4B): cuando 4.1 tiene como única respuesta "Hijo menor sin
discapacidad", el recorrido visible es **5.3 → 8.4 → 9.1 → 9.4 → fin**, tal como lo especifica
el Excel corregido (no el mockup, que marcaba toda la Sección 5 como oculta). Esto es más
granular que un simple "salto de sección": 5.3, 8.4, 9.1 y 9.4 quedan visibles porque no
dependen de que haya responsabilidad económica o de cuidado activo — preguntan por quién
cuida durante el horario laboral (5.3), franja salarial (8.4) y percepción/voluntariado
organizacional (9.1, 9.4), que aplican igual a quien solo tiene un hijo menor sin discapacidad.
- **4.1 = sole "Hijo menor sin discapacidad"**: 4.2 y 4.3 se ocultan (no hay tipo de
discapacidad ni dispositivos de apoyo que relevar), 5.1/5.2/5.4 ocultas, 5.3 VISIBLE,
S6 y S7 completas ocultas, 8.18.3 ocultas, 8.4 VISIBLE, 9.1 VISIBLE, 9.2/9.3 ocultas,
9.4 VISIBLE.
- **4.1 contiene "Ninguna de las anteriores..."**: salta directo a S10 (todo S5S9 oculto).
- **4.1 = cualquier combinación de opciones 27** (con o sin la opción 1): recorrido completo,
4.2/4.3 generan una instancia por cada vínculo 27 declarado.
--- ---
## Sección ADatos Generales (top-level) ## Sección 1Consentimiento (`S1`, pos. 10)
| Code | Tipo | Pregunta | Opciones | Notas |
|---|---|---|---|---|
| A1 | single_choice | ¿En qué departamento de Paraguay vivís?* | Asunción (Capital) · Alto Paraguay · Alto Paraná · Amambay · Boquerón · Caaguazú · Caazapá · Canindeyú · Central · Concepción · Cordillera · Guairá · Itapúa · Misiones · Ñeembucú · Paraguarí · Presidente Hayes · San Pedro | qi_zone |
| A2 | single_choice | ¿En qué ciudad vivís?* | 219 ciudades (GeoSelectWidget: dependiente de A1) | qi_city — Sprint 3 |
| A3 | text_short | ¿En qué barrio vivís? | — (texto libre, opcional) | |
| A4 | single_choice | ¿En qué área o gerencia trabajás?* | Directorio · Gerencia · Operaciones/Producción · Comercial/Ventas · Administración/Finanzas · Recursos Humanos/Cultura · Tecnología/Sistemas · Logística/Distribución · Otra área | qi_sector |
| A5 | single_choice | ¿Cuántos años tenés? | Menos de 25 años · 25 a 34 años · 35 a 44 años · 45 a 54 años · 55 años o más | qi_age_bucket |
| A6 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro | |
| A7 | single_choice | ¿Cuál es tu modalidad de trabajo actual? | Presencial · Remoto (home office) · Híbrido | |
## Sección 1 — Prevalencia: cuidado y discapacidad *(sensible)* | Code | Tipo | Pregunta | Requerida | Notas |
| Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive | |---|---|---|---|---|
| 1.1 | yes_no | Declaro que fui informado/a sobre el propósito de esta encuesta y el tratamiento de mis respuestas, y acepto participar de forma voluntaria en el diagnóstico interno de [Empresa]. | Sí | Gate de participación — ver `consent_versions` v2 |
| 1.2 | yes_no | Autorizo que mis respuestas anonimizadas se utilicen para el estudio país sobre la economía del cuidado en Paraguay. | No | Scope `macro_n1` — Modo A (CLAUDE.md regla 7) |
## Sección 2 — Datos generales (`S2`, pos. 20)
| Code | Tipo | Pregunta | Opciones | is_sensitive | Notas |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 1.1 | single_choice | trigger | ¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?* | Sí · No | ✅ | | 2.1 | single_choice | Departamento | 18 departamentos de Paraguay + Capital | no | qi (cuasi-identificador) |
| 1.2 | multiple_choice | 1.1=Sí | ¿Qué tipo de discapacidad o condición tenés? | Discapacidad física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica que genera limitaciones · Otra | | | 2.2 | single_choice | Ciudad | 219 ciudades (filtradas por 2.1 en el renderer) | no | qi |
| 1.3 | multiple_choice | 1.1=Sí | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utilizo | ✅ | | 2.3 | text_short | ¿En qué barrio vivís? | — (texto libre) | no | `hint`='Opcional.' — no entra al store analítico crudo |
| 1.4 | single_choice | 1.1=Sí | ¿Contás con certificado oficial de discapacidad? | Sí · No, pero estoy en trámite · No | | | 2.4 | single_choice | ¿Cuál es tu rango de edad? | Menos de 25 · 25-34 · 35-44 · 45-54 · 55+ | no | qi |
| 1.5 | single_choice | 1.1=Sí | ¿Tu condición afecta tu desempeño o comodidad en el trabajo? | Sí, de forma significativa · Sí, pero en aspectos puntuales · No, está bien gestionada | | | 2.5 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro | no | qi |
| 1.6 | single_choice | trigger | ¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?* | Sí · No | | | 2.6 | single_choice | ¿En qué nivel organizacional estás? | Directivo · Gerencial · Jefatura · Operativo · Auxiliar · Otro | no | qi |
| 1.7 | multiple_choice | 1.6=Sí | ¿Cuál es tu vínculo con esa persona? | Hijo/a · Cónyuge/pareja · Padre o madre · Hermano/a · Otro familiar | | | 2.7 | single_choice | ¿Cuál es tu modalidad de trabajo actual? | Presencial · Remoto · Híbrido | no | qi |
| 1.8 | multiple_choice | 1.6=Sí | ¿Qué tipo de discapacidad tiene? | Física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica severa · Otra | ✅ |
| 1.9 | multiple_choice | 1.6=Sí | ¿Qué tipo de apoyo o acompañamiento necesita principalmente? | Movilidad física · Comunicación · Apoyo escolar/educativo · Inclusión laboral · Terapias/rehabilitación · Apoyo emocional/psicosocial | ✅ |
| 1.10 | multiple_choice | 1.6=Sí | ¿Usa algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utiliza | ✅ |
| 1.11 | single_choice | 1.6=Sí | Si es niño/a o adolescente en edad escolar: ¿está escolarizado/a y enfrenta barreras de aprendizaje? | Escolarizado/a, sin barreras importantes · Escolarizado/a, enfrenta barreras o faltan ajustes · No está escolarizado/a · No aplica | ✅ |
| 1.12 | single_choice | 1.6=Sí | ¿La persona con discapacidad se encuentra trabajando actualmente? | Sí, está trabajando · No, y está buscando empleo · No, y no está buscando · No aplica | ✅ |
| 1.13 | single_choice | trigger | ¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?* | Sí · No | |
| 1.14 | single_choice | 1.13=Sí | ¿Qué nivel de dependencia tiene? (la más representativa) | Independiente, pero necesita supervisión · Dependencia leve · Dependencia moderada · Dependencia severa | ✅ |
| 1.15 | multiple_choice | 1.13=Sí | ¿Tiene una condición de salud que genere mayor necesidad de cuidado? | Demencia/Alzheimer · ACV/Parálisis · Enfermedad crónica grave · Movilidad reducida · Deterioro cognitivo · Sin condición específica | ✅ |
| 1.16 | single_choice | 1.13=Sí | ¿Vive con vos o en otra vivienda? | Vive en mi hogar · Vive en otra vivienda · Está en residencia/institución | |
## Sección 2Intensidad y rol del cuidado (top-level) ## Sección 3Prevalencia de discapacidad (`S3`, pos. 30) *(sensible)*
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
Cond.: 3.23.5 visibles solo si 3.1='Sí' (`r_show_si_s3_1`).
| Code | Tipo | Pregunta | Opciones | is_sensitive |
|---|---|---|---|---|
| 3.1 | single_choice | ¿Vos mismo/a tenés alguna discapacidad o condición de salud que pudiera afectar tu independencia o desempeño laboral? | No · Sí | ✅ trigger |
| 3.2 | multiple_choice | ¿Qué tipo de discapacidad o condición tenés? | Física/motora · Visual · Auditiva · Intelectual · Mental/psicosocial · Del habla y la comunicación · Múltiple · Vejez/osteoporosis/artrosis/otras · Amputación · Otra | ✅ |
| 3.3 | multiple_choice | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón de apoyo · Bastón blanco · Andador · Muletas · Prótesis · Tecnología asistiva · Audífonos · No utilizo | ✅ |
| 3.4 | single_choice | ¿Contás con certificado oficial de discapacidad? | Sí · No, pero estoy en trámite · No | ✅ |
| 3.5 | multiple_choice | ¿Cómo afecta tu discapacidad y/o condición limitante en tu trabajo? | No afecta mi rendimiento · Falta de adecuaciones afecta más que la discapacidad · Transporte · Accesibilidad para participar | ✅ |
## Sección 4 — Vinculación familiar (`S4`, pos. 40) *(sensible)*
Mini-wizard: 4.1 `instance_generator=true`; 4.2/4.3 `instance_of='4.1'`, una instancia por
vínculo 27 declarado en 4.1. Ocultas (4.2/4.3) si Ninguna o soloNino (`r_hide_ning_sn`).
| Code | Tipo | Pregunta | Opciones | is_sensitive |
|---|---|---|---|---|
| 4.1 | multiple_choice | ¿Cuál de las siguientes vinculaciones familiares tenés? | 1. Hijo menor sin discapacidad · 2. Hijo menor con discapacidad · 3. Hijo adulto con discapacidad · 4. Familiar menor con discapacidad · 5. Familiar mayor con discapacidad · 6. Padre/madre con movilidad reducida o 100% dependiente · 7. Otro/Especificar · 8. Ninguna de las anteriores / No tengo vinculación de cuidado | ✅ generador |
| 4.2 | multiple_choice (instancia) | ¿Qué tipo de discapacidades o limitaciones tienen tus familiares vinculados? | (mismo set que 3.2) | ✅ |
| 4.3 | multiple_choice (instancia) | ¿Qué tipo de dispositivos de apoyo utilizan tus familiares vinculados? | (mismo set que 3.3, "No utiliza" en 3ra persona) | ✅ |
## Sección 5 — Responsabilidad y cuidado (`S5`, pos. 50)
Cond.: oculta completa si Ninguna (`r_hide_ning`). 5.1/5.2/5.4 además ocultas si soloNino;
**5.3 permanece visible en soloNino** (ver "recorrido soloNino" arriba).
| Code | Tipo | Pregunta | Opciones | | Code | Tipo | Pregunta | Opciones |
|---|---|---|---| |---|---|---|---|
| 2.1 | multiple_choice ⚠️ TECH-DEBT | ¿Quién es el/la responsable principal del cuidado? | Yo soy el/la principal · Comparto con otro familiar · Hay cuidador/a pagado/a · Está en institución/centro de día | | 5.1 | multiple_choice | ¿Cuál es tu rol en el cuidado de los niños y/o personas con discapacidad o dependientes? | Proveo financieramente · Comparto gastos con familiares · Administro recursos/organizo servicios · Cuido unas horas al salir del trabajo · Me ocupo de todas las gestiones |
| 2.2 | single_choice | ¿Cuántas horas por día dedicás, en promedio, al cuidado directo? | Menos de 1h · Entre 1 y 3h · Entre 3 y 6h · Más de 6h · Varía mucho | | 5.2 | multiple_choice | ¿En qué momento del día te ocupás de gestionar/organizar el cuidado? | Antes del trabajo · Después del trabajo · Durante el trabajo (llamadas/emergencias) · En cualquier momento · Fines de semana |
| 2.3 | multiple_choice | ¿En qué momentos del día cuidás principalmente? | Antes del trabajo · Durante horario laboral (llamadas/emergencias) · Al llegar del trabajo · Durante la noche · Los fines de semana · Es continuo sin horario fijo | | 5.3 | multiple_choice | Durante el tiempo que estás en el trabajo, ¿quién se ocupa del cuidado de las personas a tu cargo? | Mi cónyuge · Otro familiar · Cuidador/a contratado/a · Centro de estudios o cuidado · Un/a vecino/a · Otro |
| 2.5 | single_choice | Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico? | Sí, frecuentemente · A veces · No, lo llevo bien | | 5.4 | single_choice | ¿Desde hace cuánto cumplís este rol de responsable o cuidador/a? | Menos de 1 año · 1-3 años · 3-5 años · Más de 5 años |
> ⚠️ TECH-DEBT (2.1): pasó de `single_choice` a `multiple_choice` — selección múltiple sin orden ## Sección 6 — Intensidad del cuidado (`S6`, pos. 60)
> de prioridad. Deuda: implementar ranking (Principal cuidador soy yo > secundario instituto >
> principal pagado > secundario familiar). Ver `TECH_DEBT.md`.
>
> **2.4 eliminada** (Cambio 6, Etapa 3I2): "¿Contás con apoyo de otras personas para el cuidado?"
> — feedback del equipo médico: pregunta similar/redundante con 2.1. Ver `OBSERVATIONS.md`.
## Sección 3 — Impacto económico del cuidado (top-level) Cond.: oculta completa si Ninguna o soloNino (`r_hide_ning_sn`).
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
> 3.4: is_sensitive=true + analyst-only (no exponer a org_admin). | Code | Tipo | Pregunta | Opciones |
|---|---|---|---|
| 6.1 | single_choice | ¿Cuántos días por semana estás a cargo del cuidado directo, en promedio? | 12 · 34 · 56 · Todos los días |
| 6.2 | single_choice | Cuando estás a cargo del cuidado, ¿cuántas horas por día dedicás, en promedio? | 12 · 34 · 56 · Más de 6 horas |
| 6.3 | multiple_choice | ¿En qué momentos del día cuidás principalmente? | Antes del trabajo · Al llegar del trabajo · Durante la noche · Fines de semana |
| 6.4 | single_choice | Además del cuidado directo, ¿cuántas horas al día necesitás estar disponible o de guardia? | 12 · 34 · 56 · Más de 6 horas · Permanente/sin horario |
## Sección 7 — Impacto en el trabajo (`S7`, pos. 70)
Cond.: oculta completa si Ninguna o soloNino (`r_hide_ning_sn`).
| Code | Tipo | Pregunta | Opciones |
|---|---|---|---|
| 7.1 | single_choice | ¿Cómo describirías tu nivel de descanso cuando te ocupás del cuidado y cómo impacta en tu desempeño? | Descanso bien · Irregular pero lo manejo · Me cuesta, afecta con frecuencia · Descanso muy poco, agotamiento afecta desempeño |
| 7.2 | single_choice | En los últimos 3 meses, ¿con qué frecuencia sentiste que el cuidado afectó tu concentración o rendimiento? | Nunca/casi nunca · Algunas veces al mes · Varias veces por semana · Casi todos los días |
| 7.3 | single_choice | En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado? | Nunca · Pocas veces (1-3/año) · Varias veces (4-10/año) · Con frecuencia (+10/año) |
| 7.4 | multiple_choice | ¿Qué aspectos de tu vida se ven más afectados por tu rol de cuidador/a? | Tiempo libre · Sueño · Vida social · Vida en pareja/familiar · Salud física · Salud emocional/mental · Desarrollo profesional · Situación económica |
| 7.5 | multiple_choice | Por tu rol de cuidado, ¿alguna vez tuviste que…? | Reducir jornada · Rechazar ascenso · Declinar viajes/extras/capacitaciones · Cambiar a puesto menos exigente · Faltar/llegar tarde reiteradamente · Pensar en dejar el trabajo · Dejar un trabajo anterior · Ninguna de las anteriores |
| 7.6 | single_choice | Pensando en los próximos 12 meses, ¿qué tan probable es que dejes o reduzcas tu trabajo por razones de cuidado? | Nada probable · Poco probable · Algo probable · Muy probable |
## Sección 8 — Impacto económico (`S8`, pos. 80)
Cond.: header y 8.4 ocultos si Ninguna (`r_hide_ning`). 8.18.3 además ocultas si soloNino.
**8.4 permanece visible en soloNino.**
| Code | Tipo | Pregunta | Opciones | is_sensitive |
|---|---|---|---|---|
| 8.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | 7 franjas, < 1M a > 15M Gs. | no |
| 8.2 | multiple_choice | ¿En qué conceptos gastás principalmente el monto expresado? | Cuidador/a particular · Centro de día/institución · Traslados · Equipamiento médico/tecnología asistiva · Servicios terapéuticos/rehabilitación · Medicamentos · Insumos personales · Otros | no |
| 8.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Solo con mi empleo · Ingresos extraordinarios · Emprendimiento · Me ayudan familiares · Me ayudan organizaciones sociales · Me ayudan vecinos/amigos · Eventos de recaudación · Subsidios estatales | no |
| 8.4 | single_choice | ¿En qué franja salarial mensual te ubicás con tu trabajo en esta empresa? *(CONFIDENCIAL)* | 9 franjas, hasta 3M a 50M+ Gs. | ✅ analyst-only, nunca en vistas de empresa (igual criterio que 3.4 en v2.1) |
## Sección 9 — Conciencia organizacional (`S9`, pos. 90)
Cond.: header, 9.1 y 9.4 ocultos si Ninguna (`r_hide_ning`). 9.2/9.3 además ocultas si soloNino.
**9.1 y 9.4 permanecen visibles en soloNino.**
| Code | Tipo | Pregunta | Opciones | Notas | | Code | Tipo | Pregunta | Opciones | Notas |
|---|---|---|---|---| |---|---|---|---|---|
| 3.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | Menos de Gs. 500.000 · Gs. 500.0001.500.000 · Gs. 1.500.0003.000.000 · Gs. 3.000.0006.000.000 · Más de Gs. 6.000.000 | | | 9.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente? | Sí, acompañado/a · Parcialmente · No, invisible · Nunca hemos tocado el tema | |
| 3.2 | multiple_choice | ¿Qué servicios o apoyos utilizás actualmente? | Terapias especializadas · Cuidador/a particular · Centro de día/institución · Transporte adaptado · Medicación alto costo · Equipamiento médico/tecnología asistiva · No utilizo servicios formales | | | 9.2 | multiple_choice (**max_select=3**) | ¿Qué apoyos de la empresa te ayudarían más? | Flexibilidad horaria/remoto · Apoyo económico/convenios · Beneficios terapias/salud · Contención emocional/psicológica · Capacitación y autocuidado · Redes de apoyo/grupos de pares · Información sobre recursos | |
| 3.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Con mi empleo de tiempo completo · Con mi empleo de tiempo parcial · Con trabajo independiente/emprendimiento · Me ayudan mis familiares · No puedo sostenerlo con mis ingresos | | | 9.3 | single_choice | ¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores dentro de la empresa? | Sí, definitivamente · Tal vez · No por ahora · Prefiero mantenerlo en privado | |
| 3.4 | single_choice | ¿En qué franja salarial mensual te ubicás? *(CONFIDENCIAL — analyst-only)* | Sueldo mínimo · Gs. 3.000.000 a 5.000.000 · Gs. 5.000.0018.000.000 · Gs. 8.000.00112.000.000 · Gs. 12.000.00115.000.000 · Gs. 15.000.00118.000.000 · Gs. 18.000.00125.000.000 · Gs. 25.000.00150.000.000 · Gs. 50.000.001 o más | is_sensitive=true; never in org_admin views | | 9.4 | single_choice | ¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo? | Sí · No | |
| 3.5 | rating | ¿Los gastos de cuidado impactan en tu situación económica personal? | 1=Sin impacto · 2=Leve · 3=Moderado · 4=Alto · 5=Muy alto | RE-INDEX |
> **Cambio 7** (3.1): se elimina "Sin gastos significativos" además de "Prefiero no decir". ## Sección 10 — Comentarios adicionales (`S10`, pos. 100)
> **Cambio 8** (3.3): "Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis
> ingresos". Breaking change para datos existentes — ver `OBSERVATIONS.md`.
> **Cambio 9** (3.4): "Sueldo mínimo Gs. 5.000.000" se divide en "Sueldo mínimo" y
> "Gs. 3.000.000 a 5.000.000". Breaking change para datos existentes — ver `OBSERVATIONS.md`.
## Sección 4 — Tiempo y descanso (top-level) Siempre visible — es el destino del salto "Ninguna de las anteriores" en 4.1.
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
| Code | Tipo | Pregunta | Opciones |
|---|---|---|---|
| 4.1 | single_choice | ¿Cómo describirías tu nivel de descanso habitual? | Descanso bien, me siento recuperado/a · Descanso de forma irregular, pero lo manejo · Me cuesta descansar, me siento cansado/a con frecuencia · Descanso muy poco, el agotamiento afecta mi día a día | RE-INDEX |
| 4.2 | multiple_choice | ¿Qué aspectos de tu vida se ven más afectados por tu rol de cuidador/a? | Tiempo libre y ocio · Sueño y descanso · Vida social y amistades · Vida en pareja/familiar · Salud física personal · Salud emocional/mental · Desarrollo profesional · Situación económica | |
| 4.3 | rating | En una escala del 1 al 5, ¿cómo evaluás tu bienestar general en este momento? | 1=Muy mal · 2=Mal · 3=Regular · 4=Bien · 5=Muy bien | RE-INDEX |
## Sección 5 — Impacto en el trabajo (top-level)
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
| Code | Tipo | Pregunta | Opciones |
|---|---|---|---|
| 5.1 | single_choice | En los últimos 3 meses, ¿con qué frecuencia sentiste que el cuidado afectó tu concentración o rendimiento? | Nunca o casi nunca · Algunas veces al mes · Varias veces por semana · Casi todos los días | RE-INDEX |
| 5.2 | single_choice | En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado? | Nunca · Pocas veces (13 veces al año) · Varias veces (410 veces al año) · Con frecuencia (más de 10 veces al año) | |
## Sección 6 — Conciencia organizacional (top-level)
> Cond. (6.1-6.3): **salto-triple** — se ocultan si 1.1='No' Y 1.6='No' Y 1.13='No'.
> 6.4-6.6: siempre visibles (destino del salto).
| Code | Tipo | Pregunta | Opciones | Notas |
|---|---|---|---|---|
| 6.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?* | Sí, me siento acompañado/a · Parcialmente, hay cierta comprensión · No, es algo bastante invisible · No, no se puede hablar del tema | RE-INDEX. Cambio 10: última opción reescrita (era "No, nunca se ha hablado del tema"). Breaking change — ver `OBSERVATIONS.md`. |
| 6.2 | multiple_choice (**max_select=3**) | ¿Qué apoyos de la empresa te ayudarían más? Elegí hasta 3 | Flexibilidad horaria o trabajo remoto · Apoyo económico o convenios/descuentos · Acceso a terapias o salud · Contención/apoyo emocional o psicológico · Capacitación y autocuidado · Redes de apoyo/grupos de pares · Información sobre recursos y servicios disponibles | ⚠️ Ranking → multiple_choice para v1. Ranking widget al backlog. |
| 6.3 | single_choice | ¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores dentro de la empresa? | Sí, definitivamente · Tal vez, si hay más personas en la misma situación · No por ahora · Prefiero mantenerlo en privado | |
| 6.4 | single_choice | ¿Conocés a algún compañero/a de trabajo que esté al cuidado de una persona con discapacidad o dependiente? | Sí, conozco a varias personas · Sí, conozco a una persona · No conozco a nadie · No estoy seguro/a | Código corregido (era 6.2 duplicado) |
| 6.5 | single_choice | ¿Considerás que la empresa debería ofrecer apoyos a quienes cuidan a una persona dependiente? | Sí, definitivamente · Tal vez, dependería del caso · No lo considero necesario · No lo sé | Código corregido (era 6.3 duplicado) |
| 6.6 | single_choice | ¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo a personas con discapacidad y/o cuidadores? | Sí · No · Tal vez | Nueva (Cambio 4b, Etapa 3I2) |
## Sección 7 — Para cerrar (top-level)
| Code | Tipo | Pregunta | Notas | | Code | Tipo | Pregunta | Notas |
|---|---|---|---| |---|---|---|---|
| 7.1 | text_long | ¿Te gustaría dejar un comentario o sugerencia especial? (opcional) | No entra al store analítico crudo (regla 4 CLAUDE.md) | | 10.1 | text_long | ¿Te gustaría dejar un comentario o sugerencia especial? (opcional) | `hint`='Opcional.' — NO entra al store analítico crudo sin categorizar (regla 4 CLAUDE.md) |
--- ---
## Mapeo a cuasi-identificadores (`responses.qi_*`) ## Totales
- `qi_age_bucket` ← A5 (ya en bucket)
- `qi_sector` ← A4 (área) 10 secciones (`type='section'`) + 40 preguntas = **50 filas** en `questions` por encuesta.
- `qi_zone` ← A1 (departamento — poblado en `submit-survey.ts` desde Etapa 2E) Preguntas por sección: S1=2, S2=7, S3=5, S4=3, S5=4, S6=4, S7=6, S8=4, S9=4, S10=1 → 40.
- `qi_city` ← A2: **Sprint 3** — se agrega como columna qi en migration futura.
Por ahora A2 se captura en `answers` como single_choice. ## Consent versions
- **v1** (`20000000-0000-0000-0000-000000000001`): texto original v2.1, preservado — no se
altera para no invalidar `consent_records` ya firmados contra esa versión (regla 5 CLAUDE.md).
- **v2** (`20000000-0000-0000-0000-000000000002`): texto revisado para el instrumento v3.0.0,
asignado a las 3 encuestas seed. Scopes: `["operativo","macro_n1"]` (Modo A).
## Discrepancias mockup vs. Excel resueltas (AP.10)
Reportadas y resueltas con la directora antes de escribir el seed:
1. **Wording de preguntas y opciones**: el Excel corregido es la fuente autoritativa en todos
los casos donde difiere del mockup (~30 discrepancias menores de texto).
2. **Recorrido soloNino**: el mockup marcaba `skipSoloNino:true` en TODA la Sección 5 (incluida
5.3). El Excel especifica un recorrido más granular: 5.3 → 8.4 → 9.1 → 9.4 → fin. Resuelto a
favor del Excel (ver "Flujo condicional — recorrido soloNino" arriba).
3. **8.3 "Eventos de recaudación"**: opción presente en el Excel, ausente en el mockup. Incluida.
## ⚠️ Decisiones pendientes para el director (NO ejecutar sin aprobación) ## ⚠️ Decisiones pendientes para el director (NO ejecutar sin aprobación)
- **Ranking widget (6.2):** actual es multiple_choice max_select=3. Cuando se implemente el
widget de ranking (drag o 3 dropdowns), 6.2 pasa a tipo `ranking` con JSONB `{rank1, rank2, rank3}`. - **Ranking widget (9.2):** actual es `multiple_choice` con `max_select=3`. Si se requiere
- **qi_city:** migration futura para agregar columna en responses y extender whitelist de agg_segment. orden de prioridad, pasa a tipo `ranking` (mismo criterio que 6.2 en v2.1).
Se hace antes de Sprint 3 (dashboard con filtro por ciudad). - **Motor genérico de formularios (Fase 2, BACKLOG-002):** este contrato es el primer caso de
- **Salary analytics:** metodología para calcular "carga económica del cuidado como % del ingreso" prueba; el intérprete declarativo se generaliza después de validar el wizard de cuidadores.
es trabajo de metodología de investigación (fuera del scope del código). - **Migración de datos v2.1 → v3:** NO se ejecuta. v2.1.0-stable permanece intacta; v3 es un
- **RE-INDEX scoring:** algoritmo de composición de los índices (descanso, bienestar, rendimiento, instrumento nuevo con seed limpio.
conciencia) — decisión antes de construir los reportes del dashboard. - **`qi_city`, RE-INDEX scoring, salary analytics:** decisiones heredadas de v2.1, aún abiertas,
- **Formulario de empresa (Relevamiento Empresa):** instrumento separado con PII de contacto. no se resuelven en esta etapa.
Ejecutar offline para las primeras empresas. Diseño del modelo de datos en el sprint del builder.

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

@@ -1,155 +1,131 @@
# ESTADO DEL PROYECTO — Datos País sobre el Cuidado # ESTADO DEL PROYECTO — Datos País sobre el Cuidado
## Documento de continuidad — 11 junio 2026 ## Documento de continuidad — 20 junio 2026
> Director: Marcos · Metodología: Maria Mersan · Contacto Fundación: Valeria Arce > Director (Product Owner): Marcos · Claude (Projects) = arquitecto/copiloto · Claude Code = implementación
> Metodología: Maria Mersan · Contacto Fundación: Valeria Arce
--- ---
## 1. URLs operativas ## 1. Versionado y ramas
| Recurso | URL | - **Versión en producción:** 2.1.0 (formulario lineal, dashboard multitenante)
|---|---| - **Versión en desarrollo:** 3.0.0 (wizard, en `main`)
| Formulario Demo | https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000001 | - **Rama de preservación:** `v2.1.0-stable` (código + dump DB) + tag `v2.1.0`
| Formulario InQuality | https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000002 | - Convención: SemVer + CHANGELOG.md (raíz) + tags en cada versión estable
| Formulario Fundación Solidaridad | https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000003 | - ADRs en `docs/adr/`. ADR-005 = esquema declarativo + estrategia dos fases.
| Dashboard — Todos | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=00000000-0000-0000-0000-000000000000 |
| Dashboard — InQuality | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=30000000-0000-0000-0000-000000000002 |
| Dashboard — Fundación Solidaridad | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=30000000-0000-0000-0000-000000000003 |
| Dashboard — Demo | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=30000000-0000-0000-0000-000000000001 |
| Control de respuestas | https://economia-cuidado.inqualityhq.com/admin/responses |
--- ---
## 2. Infraestructura — OPERATIVA ✅ ## 2. Sprint 4 — Wizard v3 — EN CURSO
- **App:** Next.js 15.3.8 (CVE-2025-66478 resuelto ✅) Arquitectura (ADR-005): el formulario es un **esquema declarativo** (datos); el frontend
- **DB:** Supabase self-hosted en Dokploy v0.29.4 es un **renderer** sin reglas de negocio; el motor interpreta el esquema.
- **VPS:** inq-mart-2, IP VPN: 172.20.5.11 Estrategia dos fases: Fase 1 (este sprint) formulario-como-esquema; Fase 2 (BACKLOG-002)
- **Repos:** motor genérico universal.
- App: `git.inquality.com.py/marcos/motor-de-encuestas` (rama main)
- Supabase stack: `git.inquality.com.py/marcos/supabase-motor-fomularios` (rama master)
- **API Supabase:** https://db.inqualityhq.com (Kong + SSL)
- **Backups:** `/var/backups/supabase-prod/`, cron 3AM, GFS (3 días diarios + domingos 14 días)
- **Studio:** solo vía SSH tunnel → `ssh -L 3001:localhost:3001 administrator@172.20.5.11`
--- ### Etapas
| Etapa | Nombre | Estado |
## 3. Instrumento — v2.1 ACTIVO
- **44 preguntas**, 8 secciones
- **Salto triple:** 1.1=No AND 1.6=No AND 1.13=No → salta a 6.4 (validado con datos reales ✅)
- **Nueva pregunta 6.6:** voluntariado en redes de apoyo (Sí/No/Tal vez)
- **2.4 eliminada**, **2.1 → multiple_choice**
- "Prefiero no decir" eliminado de todas las preguntas
- Hint "Podés marcar más de una opción" en todas las multiple_choice
- "Tecnología asistiva" en 1.3 y 1.10
---
## 4. DB — estado actual
| Tabla | Filas |
|---|---|
| organizations | 3 (Empresa Demo, InQuality, Fundación Solidaridad) |
| surveys | 3 (una por organización) |
| questions | 132 (44 × 3 surveys) |
| responses | 15 (8 InQuality + 7 Demo — todas de prueba) |
| answers | ~500 (estimado) |
---
## 5. Dashboard — multitenante operativo ✅
- Selector de empresa en el header: Todos / InQuality / Fundación Solidaridad / Demo
- "Todos" usa UUID especial `00000000-0000-0000-0000-000000000000` → pool agregado
- Implementado via flag en `_dash_responses` (migración `20260611000001`)
- Validado visualmente con n=15: KPIs, heatmap departamental, perfil población ✅
---
## 6. Vista de control /admin/responses
- Tabla plana: una fila por answer, agrupada por submission
- Colapsar/expandir por submission (Client Component island)
- Excluye: `text_long`, `is_sensitive=true` (TECH-DEBT-008 — sin auth)
- Sin columna "Tipo"
---
## 7. Método — Sprint 2 cerrado ✅
- `method/PATTERNS.md` y `method/ANTI_PATTERNS.md` creados en el repo
- C.8 promovido a estable con casos reales del proyecto
- AP.10 y AP.11 documentados con casos reales
---
## 8. Deuda técnica abierta
| ID | Descripción | Bloqueante |
|---|---|---| |---|---|---|
| TECH-DEBT-006 | Backup remoto S3 | SÍ — antes del primer dato real | | 4.0 | Preservación v2 (tag, rama, dump, CHANGELOG) | ✅ |
| TECH-DEBT-007 | auth.jwt() deprecado en GoTrue v2.189+ | No (piloto) | | 4A | Persistencia `instance_id` en answers | ✅ aplicada en prod |
| TECH-DEBT-008 | /admin/responses sin auth | No (URL no publicada) | | 4B | Esquema declarativo + seed v3 (10 secciones, 150 questions) | ✅ aplicada en prod |
| TECH-DEBT-009 | Rotar secrets Supabase post-CVE-2025-66478 | SÍ — antes del primer dato real | | 4C | Motor de resolución de flujo (puro, lib/, 16 tests) | ✅ commiteada |
| 4D | Frontend renderer (consume el motor, una preg/pantalla) | ⏳ siguiente |
| 4E | Instancias de vínculo (mini-wizard familiares) | pendiente |
| 4F | Autocomplete barrios (requiere geo_paraguay_full.json) | pendiente |
| 4G | Submit con instance_id + **validación server-side (no negociable)** | pendiente |
| 4H | RPCs de vínculo (agregados por instancia) | pendiente |
| 4I | Validación e2e + tag v3.0.0 + ADR + BACKLOG Fase 2 | pendiente |
Orden: 4D → (4E ∥ 4F) → 4G → 4H → 4I
### Estado técnico v3 en la DB (ya aplicado en prod)
- `answers.instance_id text DEFAULT 'default'`, constraint `(response_id, question_id, instance_id)`
- `questions` extendida: `instance_generator`, `instance_of`, `hint`
- 150 questions (50 × 3 surveys), instrumento v3 de 10 secciones
- 4.1 = `instance_generator`; 4.2/4.3 = `instance_of='4.1'`
- Datos de prueba v2 borrados (TRUNCATE); v3 arranca limpio
### Motor de flujo (4C) — lib/form-engine.ts
- Módulo puro, sin React/DOM. Tests en `lib/form-engine.test.mts` (16/16).
- Operadores: `eq`, `eq_any`, `not_in` (escalares); `contains`, `eq_sole` (array multiple_choice)
- Acciones: `show`, `hide` (hide = veto absoluto sobre show del mismo array)
- Reglas múltiples en un array = OR
- ID de instancia: `familiar_${posición fija de opción}` (determinista, estable)
- Secciones = pantallas propias; se ocultan solo si su fila type='section' tiene regla
### Recorridos del formulario (validados en mockup y tests)
- Completo: todos los vínculos → todas las secciones
- Solo niño sin discapacidad (`eq_sole` op 1): liviano → 5.3, 8.4, 9.1, 9.4
- Ninguna (`contains` op 8): salta a Sección 10
- 3.1='No': oculta 3.2-3.5
--- ---
## 9. Pendientes próxima sesión ## 3. Decisiones arquitectónicas clave (no reabrir)
1. TECH-DEBT-006 — backup remoto S3 1. Lógica de flujo en datos (esquema), no en frontend (ADR-005)
2. TECH-DEBT-009 — rotar secrets Supabase 2. Frontend = renderer sin reglas de negocio
3. RE Index — confirmar mapeo G1/G2/G4/G5 → códigos seed con Marcos 3. Motor de flujo (4C) corre en cliente, módulo puro testeable
4. Informe descargable — prometido en deck comercial 4. **Validación de integridad del submit = server-side, NO negociable (4G)**
5. Onboarding empresas reales — cuando validación esté lista La validación de cliente es UX, no seguridad. Datos de salud sensibles.
5. Secciones como pantallas propias (reversible — es comportamiento del motor)
6. Se extiende `questions`, no se crea tabla de definición (ADR-005)
7. Migraciones vía CLI del VPS (`docker exec -i supabase-db psql`)
8. consent_records inmutable (ADR-003); el seed nunca toca datos de respuesta
--- ---
## 10. Decisiones tomadas — no reabrir ## 4. Deuda técnica y seguridad
1. Supabase en Dokploy siempre via modo Git | ID | Descripción | Prioridad |
2. POSTGRES_PASSWORD siempre con `openssl rand -hex` |---|---|---|
3. kong.yml: una sola entrada por bloque keyauth_credentials | TECH-DEBT-006 | Backup remoto S3 | Antes de datos reales |
4. SMTP_PORT=587 siempre explícito aunque SMTP esté desactivado | TECH-DEBT-009 | Rotar secrets post-CVE | Antes de datos reales |
5. GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false siempre explícito | SEC-002 | Sprint de compliance (audit log, hashing, cifrado en reposo) | Sprint dedicado |
6. LUA_AUTH_EXPR con prefijo "Bearer " en kong-entrypoint.sh | SEC-003 | **Agujero TRUNCATE evade inmutabilidad ADR-003** — cerrar antes de datos reales | Antes de datos reales |
7. NEXT_PUBLIC_SUPABASE_URL via Build Argument en Dokploy | BACKLOG-002 | Fase 2: motor genérico de formularios | Post-deadline |
8. App conecta a https://db.inqualityhq.com
9. /admin/responses no muestra is_sensitive=true sin auth (TECH-DEBT-008) COMPLIANCE.md documenta estado de seguridad, roadmap de auditabilidad, y la decisión
10. brand_config.org_name alimenta el nombre de empresa en el formulario fundamentada de NO usar blockchain.
11. Organizaciones separadas para cada encuesta
12. conditional_rules referencian códigos de pregunta, no UUIDs
13. UUID cero `00000000-...` es el flag p_all del dashboard
14. Next.js pinneado a versión exacta `15.3.8` (sin caret)
--- ---
## 11. Contexto comercial ## 5. Infraestructura (sin cambios)
- Next.js 15.3.8 · Supabase self-hosted · Dokploy v0.29.4 · VPS inq-mart-2 (172.20.5.11)
- 2 empresas comprometidas para piloto (InQuality y Fundación Solidaridad en espacio de pruebas) - App: `economia-cuidado.inqualityhq.com` · API: `db.inqualityhq.com`
- Cronograma: JUN inicio · JUL-AGO relevamiento · SEP análisis · OCT informe + evento - Repos Gitea: `git.inquality.com.py/marcos/motor-de-encuestas`
- Meta: 20 empresas pioneras - Backups GFS locales 3AM
- Precios: Gs. 4.5M (≤50) · 7.5M (≤200) · 12M (≤400)
- GAPs deck vs producto: RE Index no implementado, informe descargable no existe
- Contacto Fundación: Valeria Arce — ddff@fundacionsolidaridad.org.py · +595 991 854333
--- ---
## 12. Prompt para nueva sesión ## 6. Contexto comercial
- 2 clientes inminentes en 10-20 días (motivan el deadline de v3)
Pegá al inicio de la nueva conversación: - InQuality y Fundación Solidaridad ya en joint venture del producto
- Datos de prueba descartables (existe dump en v2.1.0-stable)
--- ---
Sos el copiloto técnico del proyecto "Datos País sobre el Cuidado" — plataforma soberana ## 7. Próximo paso inmediato
de encuestas sobre economía del cuidado en Paraguay. Tenemos múltiples sesiones de trabajo. **Etapa 4D — Frontend renderer.** El componente consume el motor (4C) y renderiza
una pregunta por pantalla. Incluye la tarea 4.5 pospuesta de 4C (query de app/page.tsx
con hint/instance_generator/instance_of). Reemplaza lib/conditional.ts y la lógica
de secciones hardcodeada de survey-form.tsx v2.
Stack: Next.js 15.3.8 + Supabase self-hosted + Dokploy v0.29.4. ---
URL: https://economia-cuidado.inqualityhq.com
Repos en Gitea: git.inquality.com.py/marcos/
El estado completo está en `docs/estado/ESTADO_PROYECTO_JUNIO_2026.md` en el repo. ## 8. Prompt para nueva sesión
Los prompts de etapas pasadas están en `sprints/`. ```
Las reglas no negociables están en `CLAUDE.md`. Sos el arquitecto/copiloto técnico de "Datos País sobre el Cuidado", motor de
formularios soberano (Paraguay). Yo soy el product owner. Estamos en Sprint 4
construyendo el formulario wizard v3 (esquema declarativo, ADR-005).
Próximos pasos: [COMPLETAR CON LO QUE VAS A HACER] Estado completo: docs/estado/ESTADO_PROYECTO_JUNIO_2026.md en el repo.
Sprint: sprints/sprint-4/ (BRIEF + prompts de etapa + reportes).
Completadas: 4.0, 4A, 4B, 4C. Siguiente: 4D (frontend renderer).
El motor de flujo ya existe y está testeado (lib/form-engine.ts, 16 tests).
La lógica vive en datos; el frontend es renderer sin reglas.
Próximo paso: [COMPLETAR]
```

344
lib/form-engine.test.mts Normal file
View File

@@ -0,0 +1,344 @@
// Tests Node puros (sin navegador) del motor de resolución de flujo v3.
// Ejecutar: node --test lib/form-engine.test.mts
//
// Extensión .mts (no .ts): así Node ejecuta el archivo como ESM nativo con
// soporte de TypeScript sin configuración adicional, y el glob "**/*.ts" del
// tsconfig.json del proyecto (usado por `next build`/`tsc`) no lo incluye —
// evita el conflicto entre "Node necesita extensión .ts explícita en los
// imports" y "tsc rechaza imports con extensión .ts sin allowImportingTsExtensions".
import { test, describe } from 'node:test'
import assert from 'node:assert/strict'
import {
buildFormSchema,
evaluateRule,
resolveVisibility,
resolveScreens,
initialState,
setAnswer,
next,
back,
} from './form-engine.ts'
import type { ConditionalRule, FormAnswers, Question } from './form-engine.types.ts'
// ── Fixture: réplica fiel de S3S10 del seed v3 (supabase/seed.sql) ──────────
// S1/S2 no tienen conditional_rules y no aportan a estos tests; se omiten.
function q(partial: Partial<Question> & Pick<Question, 'code' | 'type' | 'prompt' | 'position'>): Question {
return {
id: partial.code,
is_sensitive: false,
required: false,
hint: null,
options: null,
max_select: null,
conditional_rules: null,
instance_generator: false,
instance_of: null,
...partial,
}
}
const VINCULOS = [
'Hijo menor sin discapacidad',
'Hijo menor con discapacidad',
'Hijo adulto con discapacidad',
'Familiar menor con discapacidad',
'Familiar mayor con discapacidad',
'Padre/madre con movilidad reducida o 100% dependiente',
'Otro / Especificar',
'Ninguna de las anteriores / No tengo vinculación de cuidado',
] as const
const NINGUNA = VINCULOS[7]
const SOLO_NINO = VINCULOS[0]
const r_show_si_s3_1: ConditionalRule[] = [
{ if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' },
]
const r_hide_ning: ConditionalRule[] = [
{ if_question: '4.1', op: 'contains', value: NINGUNA, action: 'hide' },
]
const r_hide_ning_sn: ConditionalRule[] = [
{ if_question: '4.1', op: 'contains', value: NINGUNA, action: 'hide' },
{ if_question: '4.1', op: 'eq_sole', value: SOLO_NINO, action: 'hide' },
]
function buildFixtureSchema() {
const rows: Question[] = [
// S3 — Prevalencia de discapacidad
q({ code: 'S3', type: 'section', prompt: 'Prevalencia de discapacidad', position: 30 }),
q({ code: '3.1', type: 'single_choice', prompt: '¿Tenés discapacidad?', position: 31 }),
q({ code: '3.2', type: 'multiple_choice', prompt: 'Tipo', position: 32, conditional_rules: r_show_si_s3_1 }),
q({ code: '3.3', type: 'multiple_choice', prompt: 'Dispositivos', position: 33, conditional_rules: r_show_si_s3_1 }),
q({ code: '3.4', type: 'single_choice', prompt: 'Certificado', position: 34, conditional_rules: r_show_si_s3_1 }),
q({ code: '3.5', type: 'multiple_choice', prompt: 'Impacto', position: 35, conditional_rules: r_show_si_s3_1 }),
// S4 — Vinculación familiar (4.1 generador; 4.2/4.3 instancia)
q({ code: 'S4', type: 'section', prompt: 'Vinculación familiar', position: 40 }),
q({
code: '4.1', type: 'multiple_choice', prompt: 'Vínculos', position: 41,
instance_generator: true,
options: VINCULOS.map((v) => ({ value: v, label: v })),
}),
q({ code: '4.2', type: 'multiple_choice', prompt: 'Tipo familiar', position: 42, instance_of: '4.1', conditional_rules: r_hide_ning_sn }),
q({ code: '4.3', type: 'multiple_choice', prompt: 'Dispositivos familiar', position: 43, instance_of: '4.1', conditional_rules: r_hide_ning_sn }),
// S5 — Responsabilidad y cuidado
q({ code: 'S5', type: 'section', prompt: 'Responsabilidad y cuidado', position: 50, conditional_rules: r_hide_ning }),
q({ code: '5.1', type: 'multiple_choice', prompt: 'Rol', position: 51, conditional_rules: r_hide_ning_sn }),
q({ code: '5.2', type: 'multiple_choice', prompt: 'Momento', position: 52, conditional_rules: r_hide_ning_sn }),
q({ code: '5.3', type: 'multiple_choice', prompt: 'Quién cuida en horario laboral', position: 53, conditional_rules: r_hide_ning }),
q({ code: '5.4', type: 'single_choice', prompt: 'Desde cuándo', position: 54, conditional_rules: r_hide_ning_sn }),
// S6 — Intensidad del cuidado
q({ code: 'S6', type: 'section', prompt: 'Intensidad del cuidado', position: 60, conditional_rules: r_hide_ning_sn }),
q({ code: '6.1', type: 'single_choice', prompt: 'Días/semana', position: 61, conditional_rules: r_hide_ning_sn }),
q({ code: '6.2', type: 'single_choice', prompt: 'Horas/día', position: 62, conditional_rules: r_hide_ning_sn }),
q({ code: '6.3', type: 'multiple_choice', prompt: 'Momentos', position: 63, conditional_rules: r_hide_ning_sn }),
q({ code: '6.4', type: 'single_choice', prompt: 'Disponibilidad', position: 64, conditional_rules: r_hide_ning_sn }),
// S7 — Impacto en el trabajo
q({ code: 'S7', type: 'section', prompt: 'Impacto en el trabajo', position: 70, conditional_rules: r_hide_ning_sn }),
q({ code: '7.1', type: 'single_choice', prompt: 'Descanso', position: 71, conditional_rules: r_hide_ning_sn }),
q({ code: '7.2', type: 'single_choice', prompt: 'Concentración', position: 72, conditional_rules: r_hide_ning_sn }),
q({ code: '7.3', type: 'single_choice', prompt: 'Ausencias', position: 73, conditional_rules: r_hide_ning_sn }),
q({ code: '7.4', type: 'multiple_choice', prompt: 'Aspectos afectados', position: 74, conditional_rules: r_hide_ning_sn }),
q({ code: '7.5', type: 'multiple_choice', prompt: 'Consecuencias laborales', position: 75, conditional_rules: r_hide_ning_sn }),
q({ code: '7.6', type: 'single_choice', prompt: 'Probabilidad de dejar', position: 76, conditional_rules: r_hide_ning_sn }),
// S8 — Impacto económico
q({ code: 'S8', type: 'section', prompt: 'Impacto económico', position: 80, conditional_rules: r_hide_ning }),
q({ code: '8.1', type: 'single_choice', prompt: 'Gasto mensual', position: 81, conditional_rules: r_hide_ning_sn }),
q({ code: '8.2', type: 'multiple_choice', prompt: 'Conceptos', position: 82, conditional_rules: r_hide_ning_sn }),
q({ code: '8.3', type: 'multiple_choice', prompt: 'Cómo sostiene', position: 83, conditional_rules: r_hide_ning_sn }),
q({ code: '8.4', type: 'single_choice', prompt: 'Franja salarial', position: 84, is_sensitive: true, conditional_rules: r_hide_ning }),
// S9 — Conciencia organizacional
q({ code: 'S9', type: 'section', prompt: 'Conciencia organizacional', position: 90, conditional_rules: r_hide_ning }),
q({ code: '9.1', type: 'single_choice', prompt: 'Conciencia empresa', position: 91, conditional_rules: r_hide_ning }),
q({ code: '9.2', type: 'multiple_choice', prompt: 'Apoyos', position: 92, conditional_rules: r_hide_ning_sn }),
q({ code: '9.3', type: 'single_choice', prompt: 'Grupo de apoyo', position: 93, conditional_rules: r_hide_ning_sn }),
q({ code: '9.4', type: 'single_choice', prompt: 'Voluntariado', position: 94, conditional_rules: r_hide_ning }),
// S10 — Comentarios adicionales
q({ code: 'S10', type: 'section', prompt: 'Comentarios adicionales', position: 100 }),
q({ code: '10.1', type: 'text_long', prompt: 'Comentario', position: 101 }),
]
return buildFormSchema(rows)
}
function codesOf(screens: ReturnType<typeof resolveScreens>): string[] {
return screens.filter((s) => s.kind !== 'end').map((s) => s.question!.code)
}
// ── 1. Recorrido completo ────────────────────────────────────────────────
test('recorrido completo: todos los vínculos (2-7) muestran S5-S9 enteras + 12 pantallas de instancia', () => {
const schema = buildFixtureSchema()
const answers: FormAnswers = {
'4.1:default': VINCULOS.slice(1, 7), // opciones 2..7, sin soloNino ni Ninguna
}
const screens = resolveScreens(schema, answers)
const codes = codesOf(screens)
for (const expected of ['S5', '5.1', '5.2', '5.3', '5.4', 'S6', '6.1', '6.4', 'S7', '7.1', '7.6', 'S8', '8.1', '8.4', 'S9', '9.1', '9.4']) {
assert.ok(codes.includes(expected), `falta ${expected} en recorrido completo`)
}
const instanceScreens = screens.filter((s) => s.instanceId !== null)
assert.equal(instanceScreens.length, 12, '6 familiares x 2 preguntas (4.2+4.3) = 12 pantallas')
})
// ── 2. Solo niño (eq_sole) ───────────────────────────────────────────────
test('solo niño sin discapacidad (eq_sole): recorrido liviano 5.3, 8.4, 9.1, 9.4', () => {
const schema = buildFixtureSchema()
const answers: FormAnswers = { '4.1:default': [SOLO_NINO] }
const screens = resolveScreens(schema, answers)
const codes = codesOf(screens)
// visibles: S5(header)+5.3, S8(header)+8.4, S9(header)+9.1+9.4
for (const expected of ['S5', '5.3', 'S8', '8.4', 'S9', '9.1', '9.4']) {
assert.ok(codes.includes(expected), `falta ${expected} en soloNino`)
}
// ocultas
for (const hidden of ['5.1', '5.2', '5.4', 'S6', '6.1', 'S7', '7.1', '8.1', '8.2', '8.3', '9.2', '9.3']) {
assert.ok(!codes.includes(hidden), `${hidden} no debería aparecer en soloNino`)
}
// 4.2/4.3 no se muestran (la única instancia generada queda oculta por r_hide_ning_sn)
assert.equal(screens.filter((s) => s.instanceId !== null).length, 0)
})
// ── 3. Ninguna de las anteriores (contains) ──────────────────────────────
test('"Ninguna de las anteriores" (contains): salta directo a S10, incluso combinada con otra opción', () => {
const schema = buildFixtureSchema()
const answers: FormAnswers = { '4.1:default': [VINCULOS[2], NINGUNA] } // marcada junto con otra
const screens = resolveScreens(schema, answers)
const codes = codesOf(screens)
for (const hidden of ['S5', 'S6', 'S7', 'S8', '8.4', 'S9', '9.1', '9.4']) {
assert.ok(!codes.includes(hidden), `${hidden} no debería aparecer con Ninguna marcada`)
}
assert.ok(codes.includes('S10') && codes.includes('10.1'))
// el índice de S4 (4.1) debe ser inmediatamente anterior a S10 en la lista
const idxS4 = codes.indexOf('4.1')
const idxS10 = codes.indexOf('S10')
assert.equal(idxS10, idxS4 + 1, 'S10 debe seguir inmediatamente a 4.1')
})
// ── 4. 3.1='No' oculta 3.2-3.5 ────────────────────────────────────────────
test("3.1='No' oculta 3.2-3.5; 3.1='Sí' las muestra", () => {
const schema = buildFixtureSchema()
const codesNo = codesOf(resolveScreens(schema, { '3.1:default': 'No' }))
for (const hidden of ['3.2', '3.3', '3.4', '3.5']) assert.ok(!codesNo.includes(hidden))
assert.ok(codesNo.includes('3.1'))
const codesSi = codesOf(resolveScreens(schema, { '3.1:default': 'Sí' }))
for (const shown of ['3.2', '3.3', '3.4', '3.5']) assert.ok(codesSi.includes(shown))
})
// ── 5. 3 familiares → 6 pantallas de instancia en orden por familiar ─────
test('3 familiares (seleccionados fuera de orden) generan 6 pantallas en orden de esquema, no de selección', () => {
const schema = buildFixtureSchema()
// Selección deliberadamente "fuera de orden" respecto a VINCULOS: opción 6, luego 2, luego 4.
const answers: FormAnswers = { '4.1:default': [VINCULOS[5], VINCULOS[1], VINCULOS[3]] }
const screens = resolveScreens(schema, answers)
const instanceScreens = screens.filter((s) => s.instanceId !== null)
assert.equal(instanceScreens.length, 6)
// Orden esperado por posición FIJA en options (índices 1, 3, 5 → familiar_2, familiar_4, familiar_6)
assert.deepEqual(
instanceScreens.map((s) => s.id),
[
'question:4.2:familiar_2', 'question:4.3:familiar_2',
'question:4.2:familiar_4', 'question:4.3:familiar_4',
'question:4.2:familiar_6', 'question:4.3:familiar_6',
]
)
})
test('IDs de instancia son estables: agregar un vínculo nuevo no reindexa los existentes', () => {
const schema = buildFixtureSchema()
const before = resolveScreens(schema, { '4.1:default': [VINCULOS[1], VINCULOS[4]] })
.filter((s) => s.instanceId !== null)
.map((s) => s.id)
assert.deepEqual(before, [
'question:4.2:familiar_2', 'question:4.3:familiar_2',
'question:4.2:familiar_5', 'question:4.3:familiar_5',
])
// Se agrega un vínculo ANTERIOR en el orden de opciones (índice 2) — los ids de
// familiar_2 y familiar_5 deben seguir siendo exactamente los mismos.
const after = resolveScreens(schema, { '4.1:default': [VINCULOS[1], VINCULOS[2], VINCULOS[4]] })
.filter((s) => s.instanceId !== null)
.map((s) => s.id)
assert.deepEqual(after, [
'question:4.2:familiar_2', 'question:4.3:familiar_2',
'question:4.2:familiar_3', 'question:4.3:familiar_3',
'question:4.2:familiar_5', 'question:4.3:familiar_5',
])
})
// ── 6. Navegación next/back coherente con instancias ─────────────────────
describe('navegación next/back', () => {
test('next/back recorren secuencialmente section→pregunta→instancias→fin', () => {
const schema = buildFixtureSchema()
let state = initialState(schema)
assert.equal(state.currentScreenId, 'section:S3')
state = next(state, schema)
assert.equal(state.currentScreenId, 'question:3.1')
state = setAnswer(state, '3.1', 'No')
// 3.2-3.5 ocultas → next desde 3.1 debe saltar directo a S4
state = next(state, schema)
assert.equal(state.currentScreenId, 'section:S4')
})
test('next/back son estables cuando cambian las instancias activas', () => {
const schema = buildFixtureSchema()
let state = initialState(schema)
state = setAnswer(state, '4.1', [VINCULOS[1], VINCULOS[4]]) // familiar_2, familiar_5
state.currentScreenId = 'question:4.1'
// Avanza a la primera pregunta de instancia
state = next(state, schema)
assert.equal(state.currentScreenId, 'question:4.2:familiar_2')
state = next(state, schema)
assert.equal(state.currentScreenId, 'question:4.3:familiar_2')
// Mientras está parado en 4.3:familiar_2, el usuario (vía 4D) agrega un
// vínculo anterior en el esquema (índice 2) sin moverse de pantalla.
state = setAnswer(state, '4.1', [VINCULOS[1], VINCULOS[2], VINCULOS[4]])
// El ID actual sigue existiendo y sigue siendo válido — no se desincroniza.
const next1 = next(state, schema)
assert.equal(next1.currentScreenId, 'question:4.2:familiar_3', 'avanza al nuevo familiar_3 insertado en medio')
const back1 = back(state, schema)
assert.equal(back1.currentScreenId, 'question:4.2:familiar_2', 'retrocede a familiar_2, sin verse afectado por el nuevo familiar_3')
})
test('back() desde S10 tras "Ninguna" vuelve a 4.1, saltando las secciones ocultas', () => {
const schema = buildFixtureSchema()
let state = initialState(schema)
state = setAnswer(state, '4.1', [NINGUNA])
state.currentScreenId = 'section:S10'
state = back(state, schema)
assert.equal(state.currentScreenId, 'question:4.1')
})
})
// ── Operadores: cobertura unitaria directa (vocabulario completo) ───────
describe('evaluateRule — los 5 operadores', () => {
const answers: FormAnswers = {
'3.1:default': 'Sí',
'4.1:default': ['Hijo menor con discapacidad', 'Otro / Especificar'],
'2.6:default': 'Jefatura',
}
test('eq', () => {
assert.equal(evaluateRule({ if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' }, answers), true)
assert.equal(evaluateRule({ if_question: '3.1', op: 'eq', value: 'No', action: 'show' }, answers), false)
})
test('eq_any', () => {
const rule: ConditionalRule = { if_question: ['3.1', '2.6'], op: 'eq_any', value: 'Jefatura', action: 'show' }
assert.equal(evaluateRule(rule, answers), true)
})
test('not_in', () => {
const rule: ConditionalRule = { if_question: '2.6', op: 'not_in', value: ['Directivo', 'Gerencial'], action: 'hide' }
assert.equal(evaluateRule(rule, answers), true)
const rule2: ConditionalRule = { if_question: '2.6', op: 'not_in', value: ['Jefatura'], action: 'hide' }
assert.equal(evaluateRule(rule2, answers), false)
})
test('contains', () => {
const rule: ConditionalRule = { if_question: '4.1', op: 'contains', value: 'Otro / Especificar', action: 'hide' }
assert.equal(evaluateRule(rule, answers), true)
})
test('eq_sole', () => {
const rule: ConditionalRule = { if_question: '4.1', op: 'eq_sole', value: 'Otro / Especificar', action: 'hide' }
assert.equal(evaluateRule(rule, answers), false) // hay 2 elementos, no es el único
})
})
// ── Semántica show/hide combinados ───────────────────────────────────────
describe('resolveVisibility — combinación show/hide', () => {
test('hide gana siempre sobre show, aunque show también coincida', () => {
const mixed: ConditionalRule[] = [
{ if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' },
{ if_question: '4.1', op: 'contains', value: NINGUNA, action: 'hide' },
]
const question = q({ code: 'X', type: 'single_choice', prompt: 'x', position: 1, conditional_rules: mixed })
const answers: FormAnswers = { '3.1:default': 'Sí', '4.1:default': [NINGUNA] }
assert.equal(resolveVisibility(question, answers), false)
})
test('sin reglas hide, show gana si coincide', () => {
const onlyShow: ConditionalRule[] = [{ if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' }]
const question = q({ code: 'X', type: 'single_choice', prompt: 'x', position: 1, conditional_rules: onlyShow })
assert.equal(resolveVisibility(question, { '3.1:default': 'Sí' }), true)
assert.equal(resolveVisibility(question, { '3.1:default': 'No' }), false)
})
})

197
lib/form-engine.ts Normal file
View File

@@ -0,0 +1,197 @@
// Motor de resolución de flujo v3 (ADR-005) — módulo puro, sin React ni DOM.
// Reemplaza conceptualmente a lib/conditional.ts (v2); el componente (4D) decide
// cuándo migrar. Entrada→salida determinista: dado un FormSchema y un FormAnswers,
// siempre resuelve la misma lista de pantallas.
import type {
AnswerValue,
ConditionalRule,
FormAnswers,
FormSchema,
Question,
Screen,
WizardState,
} from './form-engine.types'
const DEFAULT_INSTANCE = 'default'
export function answerKey(code: string, instanceId: string = DEFAULT_INSTANCE): string {
return `${code}:${instanceId}`
}
function getAnswer(answers: FormAnswers, code: string, instanceId = DEFAULT_INSTANCE): AnswerValue {
const v = answers[answerKey(code, instanceId)]
return v === undefined ? null : v
}
/**
* Evalúa una regla individual. `if_question` siempre se resuelve contra
* instance_id='default': en el vocabulario v3 los disparadores (3.1, 4.1)
* nunca son preguntas de instancia. Si en el futuro una regla necesitara
* compararse contra la respuesta de la PROPIA instancia, este es el punto
* a extender (hoy no hace falta — no hay caso de uso en el seed v3).
*/
export function evaluateRule(rule: ConditionalRule, answers: FormAnswers): boolean {
switch (rule.op) {
case 'eq':
return getAnswer(answers, rule.if_question) === rule.value
case 'eq_any':
return rule.if_question.some((code) => getAnswer(answers, code) === rule.value)
case 'not_in': {
const v = getAnswer(answers, rule.if_question)
if (typeof v !== 'string') return false
return !rule.value.includes(v)
}
case 'contains': {
const v = getAnswer(answers, rule.if_question)
return Array.isArray(v) && v.includes(rule.value)
}
case 'eq_sole': {
const v = getAnswer(answers, rule.if_question)
return Array.isArray(v) && v.length === 1 && v[0] === rule.value
}
default:
return false
}
}
/**
* Semántica de combinación show/hide (ver reporte en la conversación):
* 1. hide es veto absoluto — si CUALQUIER regla hide coincide, se oculta.
* 2. Si ninguna hide coincide y hay reglas show, se muestra solo si
* CUALQUIERA show coincide (OR); si hay show pero ninguna coincide, se oculta.
* 3. Si no hay ninguna regla show (solo hide, o array vacío/null), se
* muestra por defecto.
*/
export function resolveVisibility(question: Question, answers: FormAnswers): boolean {
const rules = question.conditional_rules
if (!rules || rules.length === 0) return true
const hideRules = rules.filter((r) => r.action === 'hide')
const showRules = rules.filter((r) => r.action === 'show')
if (hideRules.some((r) => evaluateRule(r, answers))) return false
if (showRules.length > 0) return showRules.some((r) => evaluateRule(r, answers))
return true
}
export interface InstanceRef {
instanceId: string
optionValue: string
}
/**
* Deriva las instancias activas desde la respuesta del generador.
* El orden e id de cada instancia se basan en la POSICIÓN FIJA de la opción
* dentro de `generator.options` (definida por el esquema), nunca en el orden
* de selección del usuario ni en un índice del array de respuesta — así una
* instancia existente conserva su id estable aunque se agregue o quite otro
* vínculo (lección del mockup: el índice se desincroniza con las instancias).
*/
export function expandInstances(generator: Question, answers: FormAnswers): InstanceRef[] {
const selected = getAnswer(answers, generator.code)
if (!Array.isArray(selected) || !generator.options) return []
const refs: InstanceRef[] = []
generator.options.forEach((opt, idx) => {
if (selected.includes(opt.value)) {
refs.push({ instanceId: `familiar_${idx + 1}`, optionValue: opt.value })
}
})
return refs
}
export function buildFormSchema(rows: Question[]): FormSchema {
return { questions: [...rows].sort((a, b) => a.position - b.position) }
}
/**
* Resuelve la lista ordenada de pantallas visibles dado el esquema y el
* estado de respuestas actual. Pura: mismo input → mismo output siempre.
* Las preguntas instance_of se expanden agrupadas POR INSTANCIA (no por
* pregunta), para reproducir el orden familiar1: 4.2→4.3, familiar2: 4.2→4.3...
*/
export function resolveScreens(schema: FormSchema, answers: FormAnswers): Screen[] {
const screens: Screen[] = []
const consumedGroups = new Set<string>()
const byCode = new Map(schema.questions.map((q) => [q.code, q] as const))
for (const q of schema.questions) {
if (q.instance_of) {
if (consumedGroups.has(q.instance_of)) continue
consumedGroups.add(q.instance_of)
const generator = byCode.get(q.instance_of)
if (!generator) continue
const instances = expandInstances(generator, answers)
const groupRows = schema.questions
.filter((r) => r.instance_of === q.instance_of)
.sort((a, b) => a.position - b.position)
for (const instance of instances) {
for (const row of groupRows) {
if (resolveVisibility(row, answers)) {
screens.push({
id: `question:${row.code}:${instance.instanceId}`,
kind: 'question',
question: row,
instanceId: instance.instanceId,
})
}
}
}
continue
}
if (q.type === 'section') {
if (resolveVisibility(q, answers)) {
screens.push({ id: `section:${q.code}`, kind: 'section', question: q, instanceId: null })
}
continue
}
if (resolveVisibility(q, answers)) {
screens.push({ id: `question:${q.code}`, kind: 'question', question: q, instanceId: null })
}
}
screens.push({ id: 'end', kind: 'end', question: null, instanceId: null })
return screens
}
export function initialState(schema: FormSchema): WizardState {
const screens = resolveScreens(schema, {})
return { answers: {}, currentScreenId: screens[0]?.id ?? 'end' }
}
export function setAnswer(
state: WizardState,
code: string,
value: AnswerValue,
instanceId: string = DEFAULT_INSTANCE
): WizardState {
return {
...state,
answers: { ...state.answers, [answerKey(code, instanceId)]: value },
}
}
/**
* Avanza una pantalla. Recalcula la lista de pantallas visibles desde las
* respuestas actuales y busca la pantalla actual por ID (nunca por índice
* guardado de antes) — así, si el cambio de respuesta ocultó/agregó
* pantallas, next() sigue aterrizando en la pantalla correcta.
*/
export function next(state: WizardState, schema: FormSchema): WizardState {
const screens = resolveScreens(schema, state.answers)
const idx = screens.findIndex((s) => s.id === state.currentScreenId)
const nextIdx = Math.max(Math.min(idx + 1, screens.length - 1), 0)
return { ...state, currentScreenId: screens[nextIdx].id }
}
export function back(state: WizardState, schema: FormSchema): WizardState {
const screens = resolveScreens(schema, state.answers)
const idx = screens.findIndex((s) => s.id === state.currentScreenId)
const prevIdx = idx <= 0 ? 0 : idx - 1
return { ...state, currentScreenId: screens[prevIdx]?.id ?? state.currentScreenId }
}

72
lib/form-engine.types.ts Normal file
View File

@@ -0,0 +1,72 @@
// Tipos del motor de resolución de flujo v3 (ADR-005).
// Módulo aislado de lib/types.ts (v2) a propósito — el componente de
// producción (survey-form.tsx) no importa nada de aquí todavía. 4D decide
// cómo conviven o se unifican los dos juegos de tipos.
export type ConditionalAction = 'show' | 'hide'
export type ConditionalRule =
| { if_question: string; op: 'eq'; value: string; action: ConditionalAction }
| { if_question: string[]; op: 'eq_any'; value: string; action: ConditionalAction }
| { if_question: string; op: 'not_in'; value: string[]; action: ConditionalAction }
| { if_question: string; op: 'contains'; value: string; action: ConditionalAction }
| { if_question: string; op: 'eq_sole'; value: string; action: ConditionalAction }
export type QuestionType =
| 'text_short' | 'text_long' | 'single_choice' | 'multiple_choice'
| 'rating' | 'nps' | 'matrix' | 'ranking' | 'date' | 'slider' | 'yes_no' | 'section'
export interface QuestionOption {
value: string
label: string
}
export interface Question {
id: string
code: string
type: QuestionType
prompt: string
position: number
is_sensitive: boolean
required: boolean
hint: string | null
options: QuestionOption[] | null
max_select: number | null
conditional_rules: ConditionalRule[] | null
/** true solo en la pregunta que genera instancias (4.1 en v3). */
instance_generator: boolean
/** code de la pregunta generadora de la que depende esta fila (4.2/4.3 → '4.1'); null si no aplica. */
instance_of: string | null
}
export interface FormSchema {
/** Filas de `questions` ordenadas por position; incluye type='section'. */
questions: Question[]
}
/** Valor de una respuesta: string (single_choice/text/yes_no) o array (multiple_choice). */
export type AnswerValue = string | string[] | number | null
/**
* Respuestas keyed por `${code}:${instance_id}`. Las preguntas normales usan
* instance_id='default'; las preguntas de instancia (instance_of) usan el id
* de su instancia (ej. 'familiar_1'), igual que `answers.instance_id` (Etapa 4A).
*/
export type FormAnswers = Record<string, AnswerValue>
export type ScreenKind = 'section' | 'question' | 'end'
export interface Screen {
/** ID estable. Nunca usar el índice de este array para identificar la pantalla actual. */
id: string
kind: ScreenKind
/** null solo para kind='end'. */
question: Question | null
/** id de instancia si la pantalla es una pregunta de instancia; null si no aplica. */
instanceId: string | null
}
export interface WizardState {
answers: FormAnswers
currentScreenId: string
}

143
sprints/sprint-4/BRIEF.md Normal file
View File

@@ -0,0 +1,143 @@
# BRIEF — Sprint 4: Formulario Wizard v3 (esquema declarativo)
> Versión objetivo: **3.0.0** · Rama: `main` · Preservación: `v2.1.0-stable`
---
## Objetivo en una frase
Construir el formulario de cuidadores como un wizard pregunta-por-pantalla cuyo
flujo, instancias y condiciones se definen en un **esquema declarativo** (datos),
renderizado por un frontend sin lógica de negocio — funcionando para los dos
clientes inminentes, y en la dirección arquitectónica del motor de formularios.
---
## Decisión arquitectónica rectora (ADR pendiente de registrar)
**El motor de formularios es el producto; la encuesta de cuidadores es su primer
caso de uso.** Por lo tanto, la lógica del flujo del formulario vive en una capa
de **definición declarativa** (datos), no en el presentador ni dispersa en columnas
sueltas de la base.
### Estrategia en dos fases (deuda intencional, registrada — no oculta)
**Fase 1 (este sprint, para el deadline de 10-20 días):**
Se construye el formulario de cuidadores con su flujo definido como esquema
declarativo persistido. El frontend es un renderer de ese esquema — sin reglas
hardcodeadas. La resolución de "qué pantalla sigue" se calcula desde esquema +
estado. NO se construye todavía el intérprete genérico universal.
**Fase 2 (sprints posteriores, post-deadline):**
Se extrae el intérprete a un motor genérico de formularios. Como el formulario de
cuidadores ya está definido como esquema declarativo, se vuelve el primer caso de
prueba del motor sin reescribir el contrato. Registrado en `BACKLOG.md`.
**Por qué esta estrategia:** entrega valor en plazo sin generar retrabajo. La
lógica en datos (no en presentación) es lo que habilita guardado parcial,
reanudación, validación consistente y progreso preciso — la mejor UX — y lo que
hace que la Fase 2 sea generalización, no reescritura.
---
## Alcance (qué entra)
- Esquema declarativo del formulario de cuidadores (flujo, instancias, condiciones como datos)
- Persistencia del esquema (en seed/DB, versionado)
- `instance_id` en `answers` (Etapa 4A — ya aplicada)
- Motor de resolución de flujo para este formulario (interpreta el esquema)
- Frontend wizard como renderer del esquema (sin reglas de negocio)
- Mini-wizard de vínculos familiares (instancias declaradas en el esquema)
- Autocomplete de barrios desde dataset INE/DGEEC
- Subtítulos del "porqué" por sección
- Submit con `instance_id` por answer
- Barra de progreso dinámica (calculada desde el esquema + estado)
- RPCs para preguntas de vínculo con instancias
- Seed v3 limpio (sin migración de datos v2)
## Fuera de alcance (explícito)
- ❌ Motor genérico universal de formularios (Fase 2 — BACKLOG)
- ❌ Backend de construcción/edición de formularios (sprint posterior)
- ❌ Migración de datos v2.1 → v3 (se decide cuando v3 esté estable)
- ❌ Tocar la rama `v2.1.0-stable`
- ❌ RE Index, informe descargable, auth de /admin/responses, backup S3
## Decisiones ya tomadas (no renegociar)
1. v3 en `main`; v2 preservado en `v2.1.0-stable` + tag `v2.1.0`.
2. Lógica de flujo en esquema declarativo (datos), NO en el frontend.
3. Frontend wizard = renderer sin reglas de negocio.
4. Estrategia dos fases: formulario-como-esquema ahora, motor genérico después.
5. v3 arranca con seed limpio, sin datos migrados.
6. `instance_id` ya implementado (4A). Instancias declaradas en el esquema.
7. Wizard pregunta-por-pantalla para todo el formulario tras consentimiento.
8. Mini-wizard de familiares: una pregunta por pantalla, badge "Familiar X de N".
9. Numeración desde Sección 1.
10. Barrio: autocomplete con datos INE, texto libre para el resto.
11. Texto de consentimiento: el del Excel del instrumento v3.
12. SemVer + CHANGELOG + tags en cada versión estable.
13. Migraciones se aplican vía CLI del VPS (`docker exec -i supabase-db psql`).
14. **Motor de flujo (4C): corre en el cliente** como módulo puro y aislado (entrada→salida
determinista, testeable con Node). Interpreta el esquema declarativo; no embebe lógica
en el componente React.
15. **Validación de integridad server-side en el submit (4G): REQUISITO NO NEGOCIABLE.**
El servidor valida contra el esquema que: las preguntas existen, los valores son opciones
válidas, las instancias corresponden a un generador real, y no se inyectan respuestas a
preguntas que el flujo nunca debió mostrar. La validación de cliente es UX, NO seguridad.
Para datos sensibles de salud, confiar solo en validación de cliente sería una falla de
seguridad. Esto NO se difiere a Fase 2.
## Criterios de éxito (checklist)
- [ ] El flujo del formulario está definido como esquema declarativo persistido
- [ ] El frontend no contiene reglas de negocio del flujo (verificable por revisión)
- [ ] El wizard funciona end-to-end, una pregunta por pantalla
- [ ] El mini-wizard de familiares genera instancias según el esquema
- [ ] Los datos se guardan con `instance_id` correcto por familiar
- [ ] El dashboard sigue funcionando (preguntas no-instancia con `instance_id='default'`)
- [ ] El autocomplete de barrios funciona para ciudades principales
- [ ] La validación visual coincide con el mockup aprobado
- [ ] `v2.1.0-stable` permanece intacta
- [ ] La deuda de Fase 2 está registrada en BACKLOG
---
## Etapas del Sprint 4
| Etapa | Nombre | Entregable | Estado |
|---|---|---|---|
| **4.0** | Preservación v2 | Tag, rama stable, dump, CHANGELOG | ✅ Completada |
| **4A** | Persistencia instance_id | Migración + DATA_MODEL | ✅ Completada |
| **4B** | Esquema declarativo + seed v3 | Esquema del formulario como datos + geo JSON | — |
| **4C** | Motor de resolución de flujo | Intérprete del esquema (este formulario) | 4B |
| **4D** | Frontend renderer | Wizard que renderiza el esquema, una preg/pantalla | 4C |
| **4E** | Instancias de vínculo | Mini-wizard de familiares desde el esquema | 4D |
| **4F** | Autocomplete barrios | Campo barrio con dataset INE | 4D |
| **4G** | Submit con instance_id | Guardado multi-instancia | 4E |
| **4H** | RPCs de vínculo | Agregados por instancia en dashboard | 4G |
| **4I** | Validación e2e + tag v3.0.0 | QA visual + release + ADR + BACKLOG Fase 2 | todas |
## Orden de ejecución
4B → 4C → 4D → (4E ∥ 4F) → 4G → 4H → 4I
---
## Definición del esquema declarativo (guía para 4B)
El esquema describe el formulario como datos. Estructura conceptual:
```
formulario:
version, secciones[]
seccion:
numero, nombre, descripcion (el "porqué")
preguntas[]
pregunta:
codigo, tipo, enunciado, hint, opciones[], required, sensitive
flujo:
condiciones_visibilidad[] (cuándo se muestra — evaluado por el motor)
genera_instancias? (si esta pregunta crea instancias, ej. 4.1)
pertenece_a_instancia_de? (de qué generador depende, ej. 4.2/4.3 → 4.1)
validaciones[]
```
La forma técnica exacta de persistir esto (columnas en `questions` +
`conditional_rules` JSONB extendido, o tabla de definición de flujo separada)
la decide la Etapa 4B según el esquema actual. Principio: declarativo, versionado,
interpretable por el motor sin conocer el caso de uso.

View File

@@ -0,0 +1,104 @@
# ETAPA 4A — Modelo de datos: instance_id en answers
## 1. Objetivo
Agregar la columna `instance_id` a la tabla `answers` y cambiar la constraint
única para soportar múltiples respuestas a la misma pregunta (una por instancia
de vínculo familiar). Es el cambio de base que habilita todo el wizard v3.
## 2. Contexto previo
- v3 se desarrolla en `main`. v2 está preservado en `v2.1.0-stable`.
- La tabla `answers` actual tiene constraint `UNIQUE (response_id, question_id)`.
- El wizard v3 genera múltiples respuestas para las preguntas 4.2 y 4.3 (una por
cada familiar declarado en 4.1). Sin `instance_id`, esas respuestas colisionan.
- Las preguntas no-vinculadas (la gran mayoría) usan `instance_id = 'default'`.
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
- Nombre de la columna: `instance_id`, tipo `text`, `DEFAULT 'default'`, NOT NULL.
- Nueva constraint: `UNIQUE (response_id, question_id, instance_id)`.
- Para preguntas normales: `instance_id = 'default'`.
- Para instancias de vínculo: `instance_id = 'familiar_1'`, `'familiar_2'`, etc.
- Esto es una migración nueva, NO modifica migraciones existentes.
- NO se migran datos v2 — el seed v3 arranca limpio (etapa posterior).
## 4. Tareas
### 4.1 Nueva migración
Crear `supabase/migrations/20260620000001_add_instance_id.sql`:
```sql
-- Sprint 4 (v3.0.0) — instance_id para respuestas multi-instancia
-- Habilita el mini-wizard de vínculos familiares (una respuesta por familiar)
-- 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 (nombre real verificado en DB)
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';
```
### 4.2 Verificar el nombre real de la constraint actual
ANTES de escribir el DROP, verificar el nombre exacto de la constraint única
actual de `answers`. El nombre asumido es `answers_response_id_question_id_key`
(convención Postgres), pero confirmar con:
```sql
SELECT conname FROM pg_constraint
WHERE conrelid = 'public.answers'::regclass AND contype = 'u';
```
Si el nombre difiere, ajustar el `DROP CONSTRAINT` en la migración.
NO ejecutar la migración en producción — solo crear el archivo.
### 4.3 Actualizar DATA_MODEL.md
Documentar el cambio en `DATA_MODEL.md`: la tabla `answers` ahora tiene
`instance_id` y la nueva constraint. Explicar el patrón 'default' vs 'familiar_N'.
## 5. Entregables (DoD)
- [ ] Migración `20260620000001_add_instance_id.sql` creada.
- [ ] Nombre real de la constraint actual verificado y usado en el DROP.
- [ ] `DATA_MODEL.md` actualizado con el nuevo esquema de `answers`.
- [ ] La migración es idempotente (`IF EXISTS` / `IF NOT EXISTS`).
- [ ] NO ejecutada en producción.
## 6. Qué reportar
```
Verificado:
- Migración creada ✓
- Nombre real de constraint actual: [nombre]
- DATA_MODEL.md actualizado ✓
- Idempotencia confirmada ✓
Asumido:
- [supuestos sobre el esquema]
NO ejecutado en producción (pendiente aprobación del director).
```
## 7. Qué NO hacer
- ❌ No ejecutar la migración en la DB de producción.
- ❌ No modificar migraciones existentes.
- ❌ No tocar el seed todavía (es la etapa 4B).
- ❌ No migrar datos de v2.
- ❌ No tocar la rama v2.1.0-stable.
## 8. Versionado
Commit sugerido al cerrar:
```
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).
Parte del Sprint 4 hacia v3.0.0.
```

View File

@@ -0,0 +1,111 @@
# ETAPA 4B — Esquema declarativo del formulario v3 + seed
## 1. Objetivo
Definir el formulario de cuidadores v3 como un esquema declarativo persistido:
extender el modelo de `questions` para soportar instanciación y nuevos operadores
de flujo, y cargar el instrumento completo (10 secciones) en el seed. La lógica del
flujo vive en estos datos, no en el frontend (ver ADR-005).
## 2. Contexto previo
- ADR-005 define la arquitectura: esquema declarativo, frontend renderer sin reglas.
- `instance_id` ya existe en `answers` (Etapa 4A).
- El esquema actual de `questions` ya tiene `conditional_rules` JSONB con operadores
`eq` / `eq_any` y acción `show`, auto-target implícito.
- El contenido exacto del instrumento v3 está validado en el mockup aprobado
(`wizard_formulario_mapeo_v3.html`) y en el Excel `Formulario_Mapeo_Corregido.xlsx`.
- El dataset de barrios está en `geo_paraguay_full.json` (va a `public/data/`).
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
- Se EXTIENDE el modelo existente, no se crea tabla de definición separada (ADR-005).
- Nuevos campos declarativos en `questions`:
- `instance_generator boolean DEFAULT false` — marca la pregunta que genera instancias (4.1)
- `instance_of text DEFAULT NULL` — código del generador del que depende (4.2/4.3 → '4.1')
- Nuevos operadores en `conditional_rules`: agregar acción `hide` y operador para
expresar el salto "Ninguna de las anteriores → salta a sección final".
- Secciones con nombre y descripción (el "porqué"): representación a definir por Claude Code
según lo más limpio (tabla `sections` o registros con metadata) — proponer antes de ejecutar.
- v3 arranca con seed LIMPIO, sin datos migrados.
- El contenido de preguntas es EXACTAMENTE el del mockup validado.
## 4. Tareas
### 4.1 Migración de esquema declarativo
Crear `supabase/migrations/20260620000002_form_schema_v3.sql`:
- Agregar `instance_generator boolean NOT NULL DEFAULT false` a `questions`.
- Agregar `instance_of text` a `questions` (nullable).
- Documentar (comentario SQL) el vocabulario extendido de `conditional_rules`:
operadores `eq`, `eq_any`, `not_in`; acciones `show`, `hide`.
- Idempotente (`IF NOT EXISTS`).
### 4.2 Representación de secciones
Proponer e implementar cómo persistir las 10 secciones con su nombre y descripción
("Texto de ayuda" del Excel). Reportar la propuesta antes de ejecutar si implica
cambio de esquema. Las descripciones de sección están en el BRIEF y el mockup.
### 4.3 Seed v3 del instrumento
Reescribir el seed del survey (o crear seed v3 separado) con las 10 secciones y todas
las preguntas EXACTAMENTE como el mockup validado:
- Sección 1: Consentimiento (2 checkboxes: requerido + opcional)
- Sección 2: Datos generales (2.1-2.7)
- Sección 3: Prevalencia discapacidad propia (3.1-3.5; 3.1='No' oculta 3.2-3.5)
- Sección 4: Vinculación familiar (4.1 generador de instancias; 4.2 y 4.3 instance_of='4.1')
- Sección 5: Responsabilidad (5.1-5.4; ocultas si "solo niño sin discapacidad")
- Sección 6: Intensidad (6.1-6.4; idem)
- Sección 7: Impacto laboral (7.1-7.6; idem)
- Sección 8: Impacto económico (8.1-8.4)
- Sección 9: Conciencia organizacional (9.1-9.4)
- Sección 10: Comentarios (10.1)
- Flujo "Ninguna de las anteriores" en 4.1 → salta a Sección 10.
- Todas las opciones, hints ("Podés marcar más de una opción"), y flags is_sensitive
exactamente como el mockup.
### 4.4 Dataset geográfico
Copiar `geo_paraguay_full.json` a `public/data/geo_paraguay.json` del repo.
### 4.5 Actualizar SEED_SPEC.md
Documentar el instrumento v3 completo como contrato (Principio 4 / AP.11).
## 5. Entregables (DoD)
- [ ] Migración de esquema declarativo creada (campos de instanciación).
- [ ] Propuesta de representación de secciones reportada (y aplicada si se aprueba).
- [ ] Seed v3 con las 10 secciones, contenido exacto del mockup.
- [ ] `instance_generator` y `instance_of` correctamente seteados en 4.1/4.2/4.3.
- [ ] Flujo declarativo: 3.1='No' oculta 3.2-3.5; "Ninguno" en 4.1 salta a S10;
"solo niño" oculta S5-S7.
- [ ] `geo_paraguay.json` en `public/data/`.
- [ ] `SEED_SPEC.md` actualizado.
- [ ] NO ejecutado en producción (solo archivos).
## 6. Qué reportar
```
Verificado:
- Migración de esquema creada ✓
- Representación de secciones: [propuesta]
- Seed v3: N preguntas, 10 secciones ✓
- instance_generator/instance_of correctos ✓
- Reglas de flujo declarativas: [listar las 3 reglas principales]
- geo JSON en public/data ✓
- SEED_SPEC.md actualizado ✓
Asumido:
- [supuestos sobre representación de secciones y operadores]
NO ejecutado en producción.
```
## 7. Qué NO hacer
- ❌ No ejecutar en producción (lo aplica el director vía CLI del VPS).
- ❌ No poner lógica de flujo en el frontend (es la etapa 4D y será renderer).
- ❌ No migrar datos v2.
- ❌ No tocar v2.1.0-stable.
- ❌ No construir el motor genérico (Fase 2).
## 8. Versionado
Commit sugerido:
```
feat(schema): esquema declarativo + seed v3 del formulario [4B]
Extiende questions con instance_generator/instance_of.
Instrumento v3 completo (10 secciones) como datos declarativos.
Flujo del formulario en el esquema, no en presentación (ADR-005).
```

View File

@@ -0,0 +1,124 @@
# ETAPA 4C — Motor de resolución de flujo (módulo puro, cliente)
## 1. Objetivo
Construir el motor que interpreta el esquema declarativo del formulario y resuelve
la secuencia de pantallas según el estado de respuestas. Es un módulo puro y aislado
en `lib/`, sin dependencias de React ni del DOM — entrada→salida determinista,
testeable con Node. El componente (Etapa 4D) lo consume; el motor no conoce al componente.
## 2. Contexto previo
- ADR-005: la lógica de flujo vive en el esquema (datos). El motor la interpreta.
- El esquema v3 está en la DB: questions con `type`, `conditional_rules`,
`instance_generator`, `instance_of`, `hint`, más filas `type='section'`.
- Decisión confirmada: el motor corre en el CLIENTE (interpretación de flujo, no validación
de seguridad — esa va server-side en 4G).
- Decisión confirmada: las secciones (`type='section'`) son PANTALLAS propias en el grafo.
- La lógica de v2 (`lib/conditional.ts`, `survey-form.tsx`) es INCOMPATIBLE y se reemplaza:
- v2 evalúa AND (`rules.every`); v3 necesita OR entre condiciones de ocultamiento.
- v2 solo conoce `eq`/`eq_any` y acción `show`; v3 necesita `contains`, `eq_sole`, `not_in`, acción `hide`.
- v2 infiere secciones por regex de prefijo; v3 las tiene como datos.
- El comportamiento esperado está validado en el mockup `wizard_formulario_mapeo_v3.html`
(función `buildVisible` y navegación) — usar como referencia de comportamiento, NO de arquitectura.
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
- Motor puro en `lib/` (proponé el nombre: ej. `lib/form-engine.ts`), sin React/DOM.
- Vocabulario de operadores: `eq`, `eq_any`, `not_in` (escalares), `contains`, `eq_sole` (array).
- Acciones: `show`, `hide`.
- Combinación: múltiples reglas en el array de una pregunta se evalúan en OR.
(Una pregunta se oculta si CUALQUIER regla `hide` aplica; se muestra si CUALQUIER regla `show` aplica.
Definí la semántica precisa de show+hide combinados y documentala.)
- Secciones = pantallas propias en el grafo.
- Instancias: 4.1 (`instance_generator=true`) genera N instancias; las preguntas con
`instance_of='4.1'` se repiten una vez por instancia activa, en orden por familiar
(familiar1: 4.2→4.3, familiar2: 4.2→4.3, ...) como en el mockup.
- Estado de respuestas con clave compuesta `question_code:instance_id` (instancias) o
`question_code` con `instance_id='default'` (normales).
## 4. Tareas
### 4.1 Tipos del motor (`lib/types.ts` ampliado o nuevo)
Definir/ampliar:
- `ConditionalRule`: operadores y acciones completos del vocabulario v3.
- `FormSchema`: la representación del formulario que el motor consume (secciones, preguntas,
reglas, instanciación) — derivada de las filas de `questions`.
- `WizardState`: respuestas (con instancias), instancia activa, pantalla actual.
- `Screen`: una pantalla resoluble (sección-intro | pregunta | pregunta-de-instancia | fin).
### 4.2 Evaluador de reglas
Reescribir la evaluación (reemplaza `lib/conditional.ts`):
- Implementar los 5 operadores y 2 acciones.
- `contains` / `eq_sole` operan sobre respuestas array (multiple_choice).
- Semántica OR documentada y correcta para el caso `r_hide_ning_sn`.
- Función pura: `(rule, answers) => boolean` y `(question, answers) => 'show'|'hide'`.
### 4.3 Resolución del grafo de pantallas
Función pura que dado el esquema + estado devuelve la lista ordenada de pantallas visibles:
- Expande instancias (4.2/4.3 por cada familiar activo declarado en 4.1).
- Aplica visibilidad (oculta secciones/preguntas según reglas).
- Incluye las pantallas de sección.
- Equivale conceptualmente al `buildVisible()` del mockup, pero como módulo puro.
### 4.4 Navegación
Funciones puras `next(state, schema)` y `back(state, schema)` que devuelven el nuevo
estado (pantalla actual actualizada), resolviendo saltos. Identificar la pantalla actual
por ID estable, no por índice (lección del mockup: el índice se desincroniza cuando
cambian las instancias).
### 4.5 Carga del esquema
Actualizar la query (`app/page.tsx`) para traer las 3 columnas nuevas
(`hint`, `instance_generator`, `instance_of`). Transformar las filas de `questions`
en el `FormSchema` que consume el motor.
### 4.6 Tests del motor (Node, sin navegador)
Tests puros que verifiquen:
- Recorrido completo (todos los vínculos) genera las pantallas esperadas.
- "Solo niño sin discapacidad" (eq_sole) → recorrido liviano correcto (5.3, 8.4, 9.1, 9.4).
- "Ninguna de las anteriores" (contains) → salta a sección 10.
- 3.1='No' oculta 3.2-3.5.
- 3 familiares → 6 pantallas de instancia en orden por familiar.
- Navegación next/back coherente cuando cambian las instancias.
## 5. Entregables (DoD)
- [ ] Motor puro en `lib/` sin dependencias de React/DOM.
- [ ] Los 5 operadores y 2 acciones implementados y testeados.
- [ ] Resolución de grafo con instancias y secciones-pantalla.
- [ ] Navegación por ID estable, no por índice.
- [ ] Query actualizada con las 3 columnas nuevas.
- [ ] Tests Node que cubren los recorridos del punto 4.6, todos en verde.
- [ ] NO se toca el componente todavía (es 4D) — el motor se entrega standalone + tests.
## 6. Qué reportar
```
Verificado:
- Motor en [archivo], puro (sin React/DOM) ✓
- Operadores implementados: eq, eq_any, not_in, contains, eq_sole ✓
- Acciones: show, hide ✓ — semántica de combinación: [descripción]
- Tests: N/N en verde
- recorrido completo ✓
- solo niño (eq_sole) → 5.3/8.4/9.1/9.4 ✓
- ninguna (contains) → salta S10 ✓
- 3.1=No oculta 3.2-3.5 ✓
- 3 familiares → 6 pantallas en orden ✓
- next/back coherente ✓
Asumido:
- [supuestos de diseño del motor]
```
## 7. Qué NO hacer
- ❌ No poner lógica de flujo en un componente React (el motor es puro, lib/).
- ❌ No tocar el componente de formulario todavía (es 4D).
- ❌ No implementar validación de seguridad acá (es 4G, server-side).
- ❌ No usar índices numéricos para la pantalla actual — usar ID estable.
- ❌ No tocar v2.1.0-stable ni el seed.
## 8. Versionado
Commit sugerido:
```
feat(engine): motor de resolución de flujo declarativo [4C]
Módulo puro que interpreta el esquema v3 y resuelve pantallas.
5 operadores (eq, eq_any, not_in, contains, eq_sole), acciones show/hide.
Instancias de vínculo y secciones-pantalla. Navegación por ID estable.
Tests Node cubren los 3 recorridos. Reemplaza lib/conditional.ts de v2.
```

View File

@@ -0,0 +1,95 @@
# PASO 0 — Preservación de v2.1.0 antes de arrancar Sprint 4
> Ejecutar ANTES de tocar nada de v3. Estos pasos preservan el estado actual
> estable como punto de retorno seguro.
---
## Objetivo
1. Crear tag `v2.1.0` sobre el commit actual de `main`.
2. Crear rama `v2.1.0-stable` que preserva el código de v2.
3. Generar un dump de la base de datos de producción y versionarlo en esa rama.
4. Crear `CHANGELOG.md` en `main`.
5. Dejar `main` listo para desarrollo de v3.
---
## Secuencia de comandos
### 1. Asegurarse de estar en main actualizado
```bash
git checkout main
git pull origin main
git status # debe estar limpio
```
### 2. Crear el tag de la versión estable actual
```bash
git tag -a v2.1.0 -m "Versión 2.1.0 — formulario lineal, dashboard multitenante, instrumento v2.1"
git push origin v2.1.0
```
### 3. Crear la rama de preservación
```bash
git checkout -b v2.1.0-stable
git push -u origin v2.1.0-stable
```
### 4. Generar el dump de la base de datos (en el VPS)
```bash
# En el VPS inq-mart-2:
docker exec supabase-db pg_dump -U postgres --clean --if-exists postgres \
| gzip > ~/dump_v2.1.0_$(date +%Y%m%d).sql.gz
# Verificar tamaño
ls -lh ~/dump_v2.1.0_*.sql.gz
```
### 5. Traer el dump a la máquina local y agregarlo a la rama
```bash
# En tu Mac, desde el repo, estando en la rama v2.1.0-stable:
mkdir -p db-snapshots
scp administrator@172.20.5.11:~/dump_v2.1.0_*.sql.gz db-snapshots/
git add db-snapshots/
git commit -m "chore(v2.1.0): snapshot de base de datos de producción
Dump completo de la DB de producción al momento del corte v2.1.0.
Incluye: 3 organizaciones, 3 surveys, 132 questions, 15 responses, ~500 answers.
Preservado antes de iniciar el desarrollo de v3 (wizard)."
git push origin v2.1.0-stable
```
### 6. Volver a main y crear el CHANGELOG
```bash
git checkout main
# Crear CHANGELOG.md (contenido en el archivo adjunto)
git add CHANGELOG.md
git commit -m "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."
git push origin main
```
---
## Verificación final
```bash
git tag -l # debe mostrar v2.1.0
git branch -a # debe mostrar main y v2.1.0-stable
git log --oneline -3 # en main, último commit es el CHANGELOG
```
A partir de acá, todo el trabajo de v3 va en `main`. La rama `v2.1.0-stable`
queda congelada como referencia y punto de retorno.
---
## Nota sobre el dump
El dump en `db-snapshots/` permite recrear la DB de v2.1.0 si fuera necesario
volver atrás. Para restaurarlo:
```bash
gunzip -c dump_v2.1.0_YYYYMMDD.sql.gz | docker exec -i supabase-db psql -U postgres -d postgres
```

File diff suppressed because one or more lines are too long

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';

View File

@@ -0,0 +1,47 @@
-- Sprint 4 (v3.0.0) — Esquema declarativo del formulario wizard
-- Extiende questions con tres campos para soporte de instanciación y secciones.
-- Ver ADR-005: la lógica del flujo vive en datos, el frontend es un renderer.
-- 1. instance_generator: marca la pregunta que genera instancias del mini-wizard.
-- true en pregunta 4.1 (vínculos familiares); false en todas las demás.
ALTER TABLE public.questions
ADD COLUMN IF NOT EXISTS instance_generator boolean NOT NULL DEFAULT false;
-- 2. instance_of: código de la pregunta generadora de la que depende esta pregunta.
-- '4.1' en preguntas 4.2 y 4.3 (se repiten una vez por familiar declarado en 4.1).
-- NULL en todas las preguntas que no forman parte del mini-wizard.
ALTER TABLE public.questions
ADD COLUMN IF NOT EXISTS instance_of text;
-- 3. hint: texto auxiliar que el renderer muestra bajo el enunciado.
-- Para type='section': descripción del "porqué" de la sección (ADR-005 §4.2, Opción C).
-- Para preguntas regulares: hint de ayuda ("Podés marcar más de una opción", etc.).
-- NULL cuando no aplica.
ALTER TABLE public.questions
ADD COLUMN IF NOT EXISTS hint text;
-- ── Vocabulario extendido de conditional_rules (referencia para el motor de flujo) ──
--
-- Formato de cada regla:
-- { "if_question": CODE | CODE[], "op": OPERATOR, "value": VALUE, "action": ACTION }
--
-- Operadores:
-- "eq" — igualdad simple: la respuesta a if_question == value
-- "eq_any" — OR lógico: AL MENOS UNO de los if_question[] == value
-- "not_in" — la respuesta a if_question NO está en value (array de strings)
-- "contains" — la respuesta (multiple_choice) CONTIENE value como uno de los
-- elementos seleccionados; útil para detectar "Ninguna de las anteriores"
-- "eq_sole" — la respuesta (multiple_choice) tiene VALUE como ÚNICO elemento
-- seleccionado; usado para el recorrido "solo niño sin discapacidad"
--
-- Acciones:
-- "show" — mostrar esta pregunta/sección cuando la condición se cumple
-- "hide" — ocultar esta pregunta/sección cuando la condición se cumple
--
-- Semántica de auto-target: la regla afecta a la pregunta/sección que la porta.
-- Sin campo "target": una pregunta declara su propia condición de visibilidad.
--
-- Ejemplo — salto "Ninguna de las anteriores" en 4.1 oculta secciones S5-S7:
-- { "if_question": "4.1", "op": "not_in",
-- "value": ["Discapacidad propia", "Familiar con discapacidad", "Adulto mayor"],
-- "action": "hide" }

File diff suppressed because it is too large Load Diff