Research — 001 Núcleo de agenda (Phase 0)
Fecha: 2026-09-29. Todas las decisiones responden al user input «stack moderno 2026 con apariencia profesional» bajo constitución v1.0.0 (el plan decide stack, no la constitución).
R1. Framework full-stack: Next.js 15 + React 19 + TypeScript estricto
- Decision: Monolito Next.js 15 (App Router) con React 19 Server Components y TypeScript 5.6
strict. Route Handlers como API. Despliegue Node 22 en contenedor. - Rationale: Un solo proyecto cubre US1–US4 (agenda SSR rápida + alta/desenlace interactivos) sin sincronizar dos codebases; Server Components dan primer pintado rápido para SC-001 (<10 s distinguir) y los Route Handlers mantienen RN1/RN2 junto a la BD. TypeScript estricto sostiene la exactitud (II) y la trazabilidad test↔spec (VI).
- Alternatives considered: Vite SPA + Express separado (rechazado: dos despliegues y duplicación de tipos para una 001 de 5 entidades); Nuxt/Vue (rechazado: equipo y ecosistema shadcn más maduro en React en 2026); FastAPI + React (rechazado: introduce frontera Python/TS sin necesidad de ML en la 001).
R2. UI profesional 2026: Tailwind CSS 4 + shadcn/ui (Radix)
- Decision: Tailwind 4 (tokens CSS-first) + componentes shadcn/ui (Button, Dialog, Calendar, Toast, Form) con tema clínico claro, WCAG AA, responsive 390px/1440px.
- Rationale: Exige VII (sin formación, sin jerga, contraste y tamaños accesibles, portátil + móvil) con el estándar 2026 para paneles profesionales; shadcn aporta accesibilidad Radix sin lock-in de librería pesada. Formato
es-ESvíaIntlen cada componente de fecha/importe. - Alternatives considered: MUI/AntD (rechazado: peso y estética genérica, más difícil de ajustar a clínica pequeña); CSS puro sin sistema (rechazado: inconsistencia y coste); ending oscuro por defecto (rechazado: mostrador clínico exige tema claro legible).
R3. Persistencia: PostgreSQL 16 + Prisma 6 (exclusión para RN1)
- Decision: PostgreSQL 16 como única BD; Prisma 6 para migraciones y acceso; restricción nativa
EXCLUDE USING gist (profesionalId WITH =, tstzrange(inicio, fin) WITH &&) WHERE (estado IN ('reservada','completada'))+ transacción serializable enPOST /api/citas. - Rationale: RN1 capital (III) incluido el caso concurrente (FR-008) no se puede garantizar solo en aplicación; la exclusión lo hace imposible a nivel de BD incluso con dos reservas en el mismo instante. Prisma mantiene migraciones versionadas y seed tipado.
timestamptz+now()único de BD sostiene RN2. - Alternatives considered: SQLite (rechazado: sin exclusión GiST, falso en concurrente); validación solo-aplicación con
SELECTprevio (rechazado: race condition); Redis lock/cola (rechazado: alcance fantasma IV para 2–5 profesionales).
R4. Dinero y tiempo exactos (II)
- Decision: Dinero como
INTEGERcéntimos en BD y dominio; redondeo mitad-hacia-arriba si alguna operación futura genera fracción; visualización conIntl.NumberFormat('es-ES',{style:'currency',currency:'EUR'})→40,00 €. Tiempo comotimestamptz; dominio en minutos; visualización conIntl.DateTimeFormat('es-ES',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})enEurope/Madrid→29/09/2026, 10:00–10:45. Precisión de minuto para RN2 (inicio < now()= pasado). - Rationale: Cumple FR-017/FR-018 y sus ejemplos límite al céntimo/minuto; evita floats y zonas ambiguas (cambio de hora estacional se muestra en hora local de clínica).
- Alternatives considered:
NUMERIC(10,2)/float euros (rechazado: errores de redondeo); guardar strings formateados (rechazado: rompe ordenación y cálculo);timestamp without tz(rechazado: ambigüedad DST).
R5. Auth v1 deuda consciente + validaciones ES
- Decision: Clave de panel por clínica guardada con hash (
argon2/bcryptjs), comparada en servidor, sin límite ni bloqueo (clarificación Q4), mensaje genéricoes-ES. Validación zod: paciente exige nombre+teléfono+email con formatos ES estrictos (teléfono+34/6-9xx, email RFC +esverosímil); nombre de servicio único por clínica (índice único parcial); prohibidoDELETEde fichas con citas (Q3/A). - Rationale: Respeta FR-002/FR-003/FR-004 y supuestos sin introducir usuarios/roles (deuda explícita para spec posterior). Mensaje genérico no revela claves válidas (US4).
- Alternatives considered: Auth provider externo / JWT por usuario (rechazado: alcance fantasma); bloqueo tras N fallos (rechazado por Q4/A); unicidad global de pacientes (rechazado por Q5/B).
R6. Semilla determinista Eleva (seedVersion=1)
- Decision:
prisma/seed.tscon PRNGmulberry32(seed=1): Clínica Eleva (jornada 09:00–20:00), 3 profesionales, 4 servicios canónicos, ~40 pacientes ES, 8 semanas pasadas (completada82 % /no_asistida10 % /cancelada8 %, tolerancia ±2) y 2 futurasreservada, solo dentro de jornada y sin solapes vigentes.Clinica.seedVersionfija la versión; regenerar =prisma migrate reset --seed. - Rationale: Cumple FR-019 y principio V (misma semilla, misma historia) con datos citables por specs y tests.
- Alternatives considered: Fixtures JSON ad hoc (rechazado: no versionado ni regenerable bit a bit); faker aleatorio sin semilla (rechazado: rompe SC-005).
R7. Estrategia de tests (VI + III)
- Decision: Vitest (unit: dinero/tiempo/solape/estados; integración: Route Handlers +
Promise.alldoble reserva concurrente; semilla determinista) + Playwright (e2e US1–US4 en 1440px y 390px, cronometrando SC-001/SC-004). Matriz test↔FR/SC enquickstart.md. Verde total + antisolape en verde = puerta de merge. - Rationale: La constitución exige prueba antisolape incluido concurrente; Playwright prueba VII/VIII tal como lo ve recepción.
- Alternatives considered: Solo unit sin e2e (rechazado: no prueba responsive ni flujos); Cypress (rechazado: Playwright mejor en 2026 para multi-viewport y trazas).