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 en specs/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ón diasLaborables es la enmienda S-06 ya aprobada, no alcance fantasma.
  • V. Datos reproducibles: sin semilla nueva; se reutiliza Eleva (seedVersion de la 001) añadiendo diasLaborables por 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: …, …–…, jornada 09:00–20:00, Lun–Vie); 24 h sin ambigüedad; DST resuelto con el desplazamiento del «ahora».
  • VIII. Español de España: es-ES en etiquetas, jornadas y docs; grep de 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

Violation Why Needed Simpler Alternative Rejected Because
— — —