Implementation Plan: Referencia temporal compartida de CitaClara (006)
Branch: 006-tiempo-referencia | Date: 2026-09-30 | Spec: specs/006-tiempo-referencia/spec.md
Input: Feature specification from specs/006-tiempo-referencia/spec.md (incl. clarificaciones 2026-09-30 Q1–Q4)
Note: Esta feature NO crea tablas ni endpoints (FR-011). Es vocabulario transversal: un único «ahora», zona canónica, precisión, fórmulas derivadas con bordes y jornada/calendario configurables. El único cambio persistente es la enmienda menor S-06 (Clinica.diasLaborables, propiedad de la 001, ya aplicada en spec + data-model de la 001; aquí se implementa migración + semilla).
Summary
Centralizar en lib/tiempo.ts (+ nuevo lib/ventanas.ts de funciones puras) las cuatro fórmulas derivadas FR-004…FR-007 con bordes exactos, el capturador único de «ahora» FR-001/FR-002 (reloj del servidor, truncado a minuto, propagado a todas las validaciones incluida la BD), la zona Europe/Madrid y el formato FR-003, y el denominador de ocupación FR-008 derivado de la jornada vigente. Migrar Clinica con diasLaborables Int[] por defecto {1,2,3,4,5} (enmienda S-06) + semilla Eleva. Migrar RN2 de la 001 de now() de BD a «ahora» propagado. Sin endpoints nuevos; los contratos son firmas TypeScript del módulo compartido. Validación con Vitest de bordes con reloj controlado (SC-002) + verificación grep de cero duplicados (SC-004).
Technical Context
Language/Version: TypeScript 5.6 strict (incl. noUncheckedIndexedAccess), Node.js 22 LTS (igual que 001; sin cambio de stack por Constitución — contexto y límites).
Primary Dependencies: date-fns-tz (fromZonedTime) + Intl (es-ES, Europe/Madrid) ya en lib/tiempo.ts; zod solo para validar jornadaInicio/Fin y diasLaborables en semilla/config; Prisma 6 para la migración S-06. Nada nuevo que instalar.
Storage: PostgreSQL 16 vía Prisma. NINGUNA tabla nueva (FR-011). Único cambio: columna Clinica.diasLaborables Int[] @default([1,2,3,4,5]) (días ISO 1=lunes…7=domingo) + actualización de prisma/seed.ts. La BD MUST NOT usar su propio now() para reglas de negocio (FR-001): las comparaciones temporales se hacen en aplicación con el «ahora» propagado; la exclusión RN1 sigue en BD pero con valores ya truncados desde app.
Testing: Vitest unit + integración ligera. Reloj controlado (fijar capturarAhora / inyección de ahora como parámetro) para SC-002: RN2 en el minuto exacto, 24 h justas dentro / 23:59 fuera, 24 h y 48 h de recordatorios dentro, semana en curso fuera y S-1…S-8 cerradas. Verificación SC-004 por grep en cada merge (cero definiciones temporales fuera de esta spec). Sin Playwright nuevo (sin UI nueva; FR-009 solo exige que la jornada/días usados se muestren junto a ocupaciones — se verifica en la 004).
Target Platform: Mismo monolito web Next.js 15 (Node 22, Docker + Postgres 16). Sin despliegue separado; librería pura + migración.
Project Type: Transversal interno (shared-library + migración de columna). Sin backend//frontend/ nuevos; código en lib/.
Performance Goals: Comparaciones O(1) por cita; cálculo de 8 semanas + denominador O(1) por profesional (aritmética de minutos, sin consultas extra). Sin objetivo p95 nuevo; no bloqueante (hereda <200 ms p95 de la 001).
Constraints: Español de España en todo texto visible (VIII): etiquetas S-N: DD/MM/AAAA–DD/MM/AAAA, tramos DD/MM/AAAA, HH:MM–HH:MM 24 h (FR-003/FR-009); zona siempre Europe/Madrid, escribir «España (península)» es defecto; truncado a minuto sin redondeo (FR-002), tolerancia cero; festivos fuera de v1 (se tratan como abiertos salvo exclusión en diasLaborables); jornada reconfigurada a mitad de periodo usa la vigente y lo declara, sin reconstrucción histórica (edge spec).
Scale/Scope: 1–N clínicas; jornada 09:00–20:00 por defecto (660 min/día × 5 días × 8 semanas = 26.400 min/profesional solo con defecto; nunca constante en código — FR-008); 8 semanas cerradas + semana en curso excluida; decenas de validaciones por operación, todas con el mismo «ahora».
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
- I. Spec First: todo comportamiento (FR-001…FR-011, US1–US3, SC-001…SC-004) vive en
specs/006-tiempo-referencia/spec.md; este plan no añade comportamiento, solo diseño de librería + migración S-06. Propietario registrado enspecs/MAPA.md(transversal-tiempo; enmienda S-06 acordada con propietario 001). - II. Exactitud numérica y temporal: truncado a minuto, bordes incluidos/excluidos escritos una sola vez (FR-004…FR-007), zona
Europe/Madrid, formatos con ejemplos límite en research/data-model/contracts; denominador derivado, nunca constante salvo defecto declarado. - III. Cero solapes: esta feature no escribe en agenda; no añade ni relaja RN1. El cambio RN2 (
now()BD → «ahora» propagado) no toca el predicado de exclusión; la suite antisolape de la 001 sigue en verde como regresión. - IV. Simplicidad / cero alcance fantasma: funciones puras en
lib/+ 1 columna + semilla; cero endpoints, cero tablas, cero dependencias nuevas, cero UI nueva. Cada firma traza a un FR (ver contracts). La migracióndiasLaborableses la enmienda S-06 ya aprobada, no alcance fantasma. - V. Datos reproducibles: sin semilla nueva; se reutiliza Eleva (
seedVersionde la 001) añadiendodiasLaborablespor defecto; ejemplos del plan usan reloj fijado explícito (30/09/2026 10:00), reproducibles sin datos ad hoc. - VI. Tests con la spec: matriz test↔regla en quickstart (SC-002 ↔ FR-004…FR-007; SC-001 ↔ FR-001; SC-003 ↔ FR-008; SC-004 ↔ FR-010); suite en verde como condición de merge; redefinir un borde fuera de aquí es defecto (FR-010).
- VII. Interfaz clara y moderna: sin jerga técnica en etiquetas visibles (
S-N: …,…–…, jornada09:00–20:00, Lun–Vie); 24 h sin ambigüedad; DST resuelto con el desplazamiento del «ahora». - VIII. Español de España:
es-ESen etiquetas, jornadas y docs;grepde SC-004 incluye variantes de zona en otro idioma como defecto.
Gates: PASS — sin violaciones que justificar. Complexity Tracking queda vacío.
Project Structure
Documentation (this feature)
specs/006-tiempo-referencia/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
│ └── ventanas-tiempo.md
└── tasks.md # Phase 2 output (/speckit.tasks — NOT created here)
Source Code (repository root)
lib/
├── tiempo.ts # EXISTENTE: ZONA_HORARIA, truncarAMinuto, formateo es-ES (se reutiliza, se amplía solo etiquetado S-N si falta)
├── ventanas.ts # NUEVO: capturarAhora + esPasado + cancelacionEnPlazo + enVentanaRecordatorios + semanasAnalitica + denominadorOcupacion (puras, testeables)
├── instantes.ts # EXISTENTE: instanteLocal / instanteDeEntrada (se reutiliza)
└── validacion.ts # EXISTENTE: ESQUEMA_HORA para jornada (se reutiliza)
prisma/
├── schema.prisma # + Clinica.diasLaborables Int[] @default([1,2,3,4,5])
└── seed.ts # + diasLaborables por defecto en Eleva
tests/
├── unit/
│ └── test_ventanas_tiempo.test.ts # NUEVO: bordes SC-002 con reloj fijado
└── integration/
└── test_ahora_unico.test.ts # NUEVO (ligero): mismo ahora en alta+antelación+ventana
Structure Decision: monolito existente, sin directorios nuevos salvo specs/006-tiempo-referencia/contracts/. Todo el código nuevo es librería pura en lib/ (testeable sin BD) + una migración de columna. Sin Route Handlers ni componentes: FR-011 lo prohíbe.
Complexity Tracking
Fill ONLY if Constitution Check has violations that must be justified