Research — 006 Referencia temporal compartida
Fuente: spec.md (FR-001…FR-011 + Q1–Q4 2026-09-30). Stack heredado de la 001: TypeScript strict, Next.js 15, Prisma + Postgres 16, date-fns-tz + Intl es-ES, Vitest. Cero NEEDS CLARIFICATION abiertos (las 4 preguntas de la spec están respondidas); esta fase consolida decisiones.
D1. Origen y propagación del «ahora» (FR-001, Q1/A)
- Decision:
capturarAhora(): Date=truncarAMinuto(new Date())una vez al inicio de cada operación (alta, desenlace,cancelarCita, selección de recordatorios, cálculo de analítica) desde el reloj del servidor de aplicación; se pasa como parámetro a todas las validaciones de esa operación. La BD usa el valor propagado; prohibidonow()/CURRENT_TIMESTAMPen reglas de negocio. - Rationale: elimina discrepancias portal/recepción/recordatorios/analítica sobre la misma cita (US1) y hace SC-001 verificable (cero discrepancias con reloj fijado). Tolerancia cero + truncado (no redondeo) hacen el borde determinista.
- Alternatives considered:
now()de Postgres por regla (estado actual de RN2 en la 001) — rechazado: dosnow()en la misma operación pueden caer en minutos distintos y servidor/BD pueden derivar; además impide tests con reloj controlado. Reloj por chequeo (no capturado) — rechazado por US1-AS3 (el reloj avanza durante la operación).
D2. Precisión de minuto y truncado (FR-002)
- Decision: reutilizar
truncarAMinutoexistente (setUTCSeconds(0,0)sobre clon) encapturarAhoray en toda entrada (instanteDeEntrada,instanteLocal). Dos instantes del mismo minuto son el mismo minuto. - Rationale: ya implementado y testeado en
lib/tiempo.ts; evita que segundos/milisegundos decidan un borde de 24 h o un «pasado» en el minuto exacto (edge spec: cita en el mismo minuto no es pasado). - Alternatives considered: redondeo al minuto más cercano — rechazado: 23 h 59 min 40 s redondearía a 24 h y entraría en plazo indebidamente. Comparación con segundos — rechazado: rompe SC-002 (bordes escritos en minutos).
D3. Zona canónica y cambio de hora estacional (FR-003, edge DST)
- Decision:
ZONA_HORARIA = 'Europe/Madrid'ya centralizada enlib/tiempo.ts; toda construcción usafromZonedTime(fecha+hora, ZONA)y toda lectura usaIntlcontimeZone: Europe/Madrid. La hora ambigua del día del cambio se resuelve con el desplazamiento vigente en el «ahora» capturado (es decir: se comparan instantes UTC, se muestran en Madrid). - Rationale: comparar en UTC elimina la ambigüedad de la hora repetida (02:30 dos veces); mostrar siempre con día/mes/año+hora/minutos 24 h cumple Constitución II.
date-fns-tz+Intlya son dependencia aprobada, sin nada nuevo. - Alternatives considered: guardar strings locales sin zona — rechazado: ordenación y diferencias de 24 h fallan en DST. Guardar en UTC y mostrar sin zona explícita — rechazado: la spec exige
Europe/Madridnominal; otro nombre es defecto (FR-003).
D4. Fórmulas derivadas y bordes (FR-004…FR-007, Q3/A, US2)
- Decision: escribir cada fórmula una sola vez en
lib/ventanas.tscomo función pura(inicio, ahora) => boolean(o cálculo de semanas), con los bordes literales de la spec:- RN2 pasado:
inicio < ahora(igual no es pasado). - Cancelación:
inicio − ahora ≥ 24 h(24 h 00 min dentro, 23 h 59 min fuera, sin tope). - Recordatorios:
24 h ≤ inicio − ejecución ≤ 48 h(ambos incluidos). - Analítica: semana en curso = semana natural lunes–domingo que contiene hoy en Madrid, recalculada en cada llamada (
diasLaborablesno altera bordes); historia = 8 anteriores cerradas S-8…S-1; la en curso nunca entra.
- RN2 pasado:
- Rationale: Q3/A prohíbe repetir fórmulas en cada spec; un único módulo hace SC-004 verificable por grep y SC-002 testeable en un solo sitio. Funciones puras ⇒ tests sin BD ni reloj real.
- Alternatives considered: duplicar cada borde en 001/portal/recordatorios/analítica — rechazado por FR-010 (redefinir fuera es defecto). Expresar ventanas en horas naturales «mismo día» — rechazado: la spec exige diferencias exactas de 24/48 h con precisión de minuto.
D5. Formato y etiquetado (FR-003, FR-009, Q4/A)
- Decision: reutilizar
formatearFechaHora/formatearHora/formatearTramo(es-ES, 24 h, sin espacios inseparables). AñadiretiquetarSemana(n, inicio, fin)→S-N: DD/MM/AAAA–DD/MM/AAAAyetiquetarJornada(jornadaInicio, jornadaFin, diasLaborables)→09:00–20:00, Lun–Vie, derivados de la ventana calculada, nunca constantes. - Rationale: Q4/A exige etiquetas derivadas;
Intlya normalizaes-ES. Abreviaturas de día en español de España (Lun…Dom) para el denominador visible (FR-008/FR-009). - Alternatives considered: etiquetas constantes o ISO (
W39) — rechazadas: Q4/A y Constitución VIII piden forma legible de clínica sin jerga.
D6. Jornada, calendario y denominador honesto (FR-008, Q2/A, US3)
- Decision:
Clinica.diasLaborables Int[]días ISO 1–7, defecto{1,2,3,4,5}, junto ajornadaInicio/Fin(defecto 09:00/20:00). Denominador =minutosJornada × nºDíasLaborables × 8, calculado con la configuración vigente y mostrado junto a cada porcentaje; prohibido hardcodear 26.400 en ningún caso (con defecto se calcula igual). Festivos fuera de v1 (abiertos salvo exclusión explícita). Reconfiguración a mitad de periodo: se usa la vigente y se declara, sin historia. - Rationale: Q2/A lo ubica en la entidad Clínica de la 001 (enmienda S-06, ya reflejada en su data-model); derivar el denominador evita ocupaciones infladas en clínicas con sábados (US3).
Int[]en Postgres es consultable y migra sin tabla nueva, coherente con FR-011. - Alternatives considered: constante 5 días / 26.400 min — rechazada por FR-008 (MUST NOT). Tabla de festivos en v1 — rechazada (fuera de alcance, edge spec). Reconstrucción histórica de jornada — rechazada (edge spec: vigente + declaración).
D7. Prohibición de redefinir y verificación (FR-010, SC-004)
- Decision:
lib/ventanas.tses la única definición ejecutable; las demás specs solo citan006 FR-00Xpor nombre. Verificación canónica en cada merge (misma en QS-7 y T021):grep -rn "now()\|CURRENT_TIMESTAMP\|Europe/\|España (península)\|24 *h\|48 *h\|S-[18]" --include="*.ts" lib app prisma | grep -v "lib/\(tiempo\|ventanas\).ts"debe dar cero definiciones rivales fuera de citas006 FR-00X(revisión manual de cada hit;diasLaborablesno se grepa: es campo legítimo deClinica). - Rationale: convierte SC-004 en chequeo mecánico barato, coherente con Constitución VI (trazabilidad) y IV (cero duplicación).
- Alternatives considered: revisión solo manual — rechazada: no escala con agentes en paralelo (Constitución I).