Tasks: Referencia temporal compartida de CitaClara (006)

Input: Design documents from specs/006-tiempo-referencia/ Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/ventanas-tiempo.md, quickstart.md Tests: YES — included per plan.md (Vitest bordes SC-002 con reloj controlado) y Constitución VI (trazabilidad test↔regla). Sin Playwright nuevo (sin UI nueva, FR-011). Organization: Tasks grouped by user story. Cada historia es un incremento independiente y testeable.

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to (US1, US2, US3)
  • Include exact file paths in descriptions

Path Conventions

Monolito existente: lib/ (librería pura), prisma/ (migración + semilla), app/api/ (Route Handlers), tests/unit/ + tests/integration/. Sin backend//frontend/ nuevos (plan.md Structure Decision). FR-011 prohíbe endpoints y tablas nuevas.


Phase 1: Setup (Shared Infrastructure)

Purpose: Toolchain verificado e inventario de la base existente que se reutiliza sin reinstalar nada

  • T001 Verificar toolchain Node 22 + dependencias instaladas (date-fns-tz, zod, Prisma 6, Vitest) contra package.json con npm ls date-fns-tz zod y node --version en repo root
  • T002 [P] Inventariar helpers reutilizables en lib/tiempo.ts (ZONA_HORARIA, truncarAMinuto, formatearFechaHora, formatearHora, formatearTramo, aMinutos) y confirmar que no falta ninguna dependencia nueva (plan.md: nada nuevo que instalar)
  • T003 [P] Inventariar validadores reutilizables en lib/validacion.ts (ESQUEMA_HORA para jornada) y lib/instantes.ts (instanteLocal, instanteDeEntrada) como base de lib/ventanas.ts

Phase 2: Foundational (Blocking Prerequisites)

Purpose: Migración S-06 + semilla que TODAS las historias necesitan (el diasLaborables parametriza semanasAnalitica de US2 y el denominador de US3)

⚠️ CRITICAL: No user story work can begin until this phase is complete

  • T004 Añadir columna diasLaborables Int[] @default([1, 2, 3, 4, 5]) al modelo Clinica en prisma/schema.prisma (días ISO 1=lunes…7=domingo; defecto {1,2,3,4,5}; enmienda S-06 propiedad de la 001)
  • T005 Crear migración SQL prisma/migrations/XXXX_add_dias_laborables_clinica/migration.sql con ALTER TABLE "Clinica" ADD COLUMN "diasLaborables" INTEGER[] NOT NULL DEFAULT '{1,2,3,4,5}' y ejecutar npx prisma migrate dev + npx prisma generate
  • T006 Añadir diasLaborables con defecto [1,2,3,4,5] en la creación de Eleva en prisma/seed.ts (reutilizar semilla Eleva + seedVersion de la 001, sin semilla nueva; validación ESQUEMA_HORA para jornada ya existe)
  • T007 [P] Añadir esquema zod ESQUEMA_DIAS_LABORABLES (array de enteros 1–7, defecto [1,2,3,4,5], "festivos fuera de v1 → abiertos salvo exclusión explícita") en lib/validacion.ts

Checkpoint: Foundation ready — npx prisma migrate status en verde y semilla Eleva con diasLaborables; las historias pueden empezar en paralelo


Phase 3: User Story 1 — Un único «ahora» para todas las validaciones (Priority: P1) 🎯 MVP

Goal: Un único instante capturado al inicio de cada operación (reloj del servidor, truncado a minuto) usado por pasado, antelaciones y ventanas sin discrepancias entre features (FR-001/FR-002, SC-001)

Independent Test: Fijar reloj en 30/09/2026 10:00, validar alta en pasado límite + cancelación al borde de 24 h + ventana 24–48 h, avanzar el reloj 2 min a mitad de operación y comprobar que las tres usan 30/09/2026, 10:00 (QS-1; US1-AS1…AS3)

Tests for User Story 1

NOTE: Write these tests FIRST, ensure they FAIL before implementation

  • T008 [P] [US1] Test unitario RN2 al minuto en tests/unit/test_ventanas_tiempo.test.ts (QS-2: con ahora = 30/09/2026, 10:00 Europe/Madrid, esPasado(09:59) ⇒ true, esPasado(10:00) ⇒ false; "el borde es estricto: solo lo anterior a «ahora» es pasado"; segundos truncados no deciden)
  • T009 [P] [US1] Test integración ahora-único en tests/integration/test_ahora_unico.test.ts (QS-1: alta + cancelacionEnPlazo + enVentanaRecordatorios en una misma operación con ahora inyectado; avanzar reloj 2 min y repetir; "las tres usan el instante capturado al inicio, no el instante de cada chequeo")

Implementation for User Story 1

  • T010 [US1] Implementar capturarAhora(fecha?: Date) (= truncarAMinuto(new Date()), "reloj del servidor de aplicación", "truncar a precisión de minuto (sin redondeo)", "tolerancia cero") y esPasado(inicio, ahora) (inicio < ahora, "igual no es pasado") en lib/ventanas.ts
  • T011 [US1] Migrar RN2 en app/api/citas/route.ts: sustituir SELECT now() dentro de la transacción por capturarAhora() capturado ANTES de prisma.$transaction y esPasado(inicio, ahora) truncado a minuto (la BD usa el valor propagado, nunca su propio now(); predicado de exclusión RN1 intacto)
  • T012 [US1] Marcar ahoraDeLaBaseDeDatos() en lib/api.ts como deprecated hacia capturarAhora() de lib/ventanas.ts (prohibido new Date(), now() o CURRENT_TIMESTAMP dentro de reglas de negocio, salvo dentro de capturarAhora()) y verificar que la suite antisolape de la 001 sigue en verde (npm run test -- test_antisolape)

Checkpoint: US1 funciona y se testea sola — npm run test -- test_ventanas_tiempo test_ahora_unico en verde; cero discrepancias pasado/futuro (SC-001)


Phase 4: User Story 2 — Ventanas derivadas con bordes escritos una sola vez (Priority: P1)

Goal: Las cuatro fórmulas FR-004…FR-007 viven solo en lib/ventanas.ts; consumidoras (001, portal, recordatorios, analítica) citan por nombre sin copiar bordes (FR-010, Q3/A)

Independent Test: Cada ventana en sus bordes exactos contra esta spec: RN2 minuto exacto, 24 h 00 min dentro / 23 h 59 min fuera, 24 h y 48 h dentro, S-1…S-8 con semana en curso fuera (QS-2…QS-5; US2-AS1…AS4)

Tests for User Story 2

NOTE: Write these tests FIRST, ensure they FAIL before implementation

  • T013 [P] [US2] Tests unitarios de bordes 24 h y 24–48 h en tests/unit/test_ventanas_tiempo.test.ts (QS-3: cancelacionEnPlazo(ahora + 24 h 00 min) ⇒ true, +23 h 59 min ⇒ false, +30 días ⇒ true sin tope; QS-4: con ejecucion = 01/10/2026, 00:00, +24 h ⇒ true, +48 h ⇒ true, +23 h 59 min ⇒ false, +48 h 01 min ⇒ false; "ambos bordes incluidos, precisión de minuto, Europe/Madrid")
  • T014 [US2] Test unitario 8 semanas cerradas en tests/unit/test_ventanas_tiempo.test.ts (QS-5: abrir miércoles 30/09/2026, semanasAnalitica(hoy) devuelve 8 filas S-8…S-1 lunes–domingo anteriores, "la semana en curso MUST NOT entrar", cada fila S-N: DD/MM/AAAA–DD/MM/AAAA derivada sin constantes; secuencial con T013: mismo fichero)

Implementation for User Story 2

  • T015 [US2] Implementar cancelacionEnPlazo(inicio, ahora) (inicio − ahora ≥ 24 h, "borde de 24 h incluido, precisión de minuto", "sin tope superior") y enVentanaRecordatorios(inicio, ejecucion) (24 h ≤ inicio − ejecución ≤ 48 h, "ambos bordes incluidos") en lib/ventanas.ts (secuencial con T016/T017: mismo fichero)
  • T016 [US2] Implementar semanasAnalitica(hoy, diasLaborables?) en lib/ventanas.ts ("semana en curso = semana natural lunes–domingo que contiene hoy en Madrid, recalculada en cada llamada; marco siempre lunes–domingo con independencia de diasLaborables; historia = 8 anteriores cerradas S-8…S-1; la en curso nunca entra"; comparaciones sobre instantes UTC, DST resuelto con desplazamiento del «ahora»)
  • T017 [US2] Implementar etiquetarTramo(inicio, fin) (DD/MM/AAAA, HH:MM–HH:MM, delega en formatearTramo existente) y etiquetas S-N: DD/MM/AAAA–DD/MM/AAAA con abreviaturas es-ES en lib/ventanas.ts (FR-003/FR-009, Q4/A; zona siempre Europe/Madrid, "escribir «España (península)» es defecto")

Checkpoint: US1 + US2 funcionan independientes — bordes SC-002 al 100 % en npm run test -- test_ventanas_tiempo


Phase 5: User Story 3 — Jornada y calendario configurables con denominador honesto (Priority: P2)

Goal: La clínica configura jornada + días de apertura; la ocupación deriva su denominador de la configuración vigente y lo muestra junto al porcentaje (FR-008/FR-009, SC-003)

Independent Test: Con defecto 09:00–20:00 Lun–Vie el denominador es 26.400 min y se muestra «09:00–20:00, Lun–Vie»; reconfigurar a Lun–Sáb (vía semilla: editar prisma/seed.ts + migrar) cambia el denominador a 31.680 y se declara (QS-6; US3-AS1…AS3)

Tests for User Story 3

  • T018 [P] [US3] Tests unitarios de denominador en tests/unit/test_ventanas_tiempo.test.ts (QS-6: denominadorOcupacion("09:00","20:00") ⇒ { minutos: 26400, etiquetaJornada: "09:00–20:00, Lun–Vie" }; con Lun–Sáb ⇒ { minutos: 31680, …Sáb } distinto de 26.400; "MUST NOT hardcodear 26.400 en ningún caso, con defecto se calcula igual")

Implementation for User Story 3

  • T019 [US3] Implementar denominadorOcupacion(jornadaInicio, jornadaFin, diasLaborables?, semanas = 8) en lib/ventanas.ts (denominadorMin = minutosJornada × nºDíasLaborables × 8 con la configuración vigente; "reconfiguración a mitad de periodo: vigente + declaración, sin historia"; ejemplo 660 × 6 × 8 = 31.680 con sábados)
  • T020 [US3] Implementar etiquetarJornada(jornadaInicio, jornadaFin, diasLaborables) en lib/ventanas.ts (09:00–20:00, Lun–Vie, días Lun…Dom es-ES derivados de la ventana, nunca constantes; la jornada/días usados MUST indicarse visiblemente junto a ocupaciones)

Checkpoint: Todas las historias funcionan independientes — npm run test -- test_ventanas_tiempo test_ahora_unico en verde


Phase 6: Polish & Cross-Cutting Concerns

Purpose: Verificación transversal SC-004 + puertas de calidad (Constitución II/VI/VIII)

  • T021 Ejecutar verificación SC-004 de cero duplicados con 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" en repo root (canónica D7/QS-7; esperado: cero definiciones rivales fuera de lib/tiempo.ts, lib/ventanas.ts y citas 006 FR-00X, cada hit se revisa a mano; "redefinir un borde o una ventana fuera de aquí es defecto", FR-010)
  • T022 [P] Ejecutar validación quickstart QS-1…QS-7 completa y npm run test -- test_ventanas_tiempo test_ahora_unico test_rn2_pasado test_antisolape más npm run typecheck en repo root (suite en verde como condición de merge; regresión antisolape 001 intacta)
  • T023 [P] Ejecutar npm run lint y npm run format:check en repo root y corregir desviaciones en lib/ventanas.ts, prisma/seed.ts, app/api/citas/route.ts (textos visibles en español de España, VIII; etiquetas sin jerga, VII)

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies — can start immediately
  • Foundational (Phase 2): Depends on Setup — BLOCKS all user stories (migración diasLaborables parametriza US2/US3)
  • User Stories (Phase 3+): All depend on Foundational completion
    • US1 (P1) → US2 (P1, necesita capturarAhora + esPasado de US1) → US3 (P2, necesita jornada/semana de US1/US2)
    • Con un solo desarrollador: orden P1 → P1 → P2
  • Polish (Phase 6): Depends on all desired user stories being complete

User Story Dependencies

  • User Story 1 (P1): Can start after Foundational — no dependencies on other stories
  • User Story 2 (P1): Depends on US1 (capturarAhora para inyectar ahora/ejecucion; esPasado como patrón de borde) — testeable sola con reloj fijado
  • User Story 3 (P2): Depends on US1/US2 (usa diasLaborables y semana natural) — testeable sola reconfigurando jornada

Within Each User Story

  • Tests MUST be written and FAIL before implementation (T008/T009 antes de T010; T013/T014 antes de T015)
  • Funciones puras antes que cableado (lib/ventanas.ts antes que app/api/citas/route.ts)
  • Core implementation before integration (RN2-migración T011 después de T010)
  • Story complete before moving to next priority

Parallel Opportunities

  • T002 + T003 (inventarios tiempo / validacion+instantes) en paralelo
  • T007 (zod diasLaborables) en paralelo con T004–T006 (migración + semilla, distinto fichero)
  • T008 + T009 (unit RN2 + integración ahora-único) en paralelo (distintos ficheros); T013 → T014 secuencial (mismo fichero tests/unit/test_ventanas_tiempo.test.ts)
  • T015 (cancelacion+recordatorios) secuencial con T016/T017 — mismo fichero lib/ventanas.ts, coordinar por función (crear esqueleto primero)
  • T022 + T023 (tests/typecheck + lint/format) en paralelo

Parallel Example: User Story 1

# Launch all tests for User Story 1 together:
Task: "Test unitario RN2 al minuto en tests/unit/test_ventanas_tiempo.test.ts (QS-2)"
Task: "Test integración ahora-único en tests/integration/test_ahora_unico.test.ts (QS-1)"

Parallel Example: User Story 2

# Launch all tests for User Story 2 together:
Task: "Tests unitarios de bordes 24 h y 24–48 h en tests/unit/test_ventanas_tiempo.test.ts (QS-3/QS-4)"
Task: "Test unitario 8 semanas cerradas en tests/unit/test_ventanas_tiempo.test.ts (QS-5)"

Implementation Strategy

MVP First (User Story 1 Only)

  1. Complete Phase 1: Setup (T001–T003)
  2. Complete Phase 2: Foundational (T004–T007, migración S-06 + semilla)
  3. Complete Phase 3: User Story 1 (T008–T012, capturarAhora + esPasado + RN2 propagado)
  4. STOP and VALIDATE: npm run test -- test_ventanas_tiempo test_ahora_unico test_rn2_pasado + QS-1/QS-2; cero discrepancias (SC-001)
  5. Deploy/demo if ready (vocabulario mínimo consumible por la 001)

Incremental Delivery

  1. Setup + Foundational → base migrada y sembrada
  2. Add US1 → mismo «ahora» en toda operación → validar (MVP)
  3. Add US2 → cuatro ventanas con bordes únicos → validar QS-3…QS-5 (SC-002)
  4. Add US3 → denominador honesto → validar QS-6 (SC-003)
  5. Polish → grep SC-004 + typecheck/lint → merge

Parallel Team Strategy

Un solo agente por spec (Constitución: un agente, una spec). Dentro de la 006, con dos desarrolladores tras Foundational:

  1. Ambos completan Setup + Foundational juntos
  2. Dev A: US1 (T008–T012) → US3 (T018–T020)
  3. Dev B: US2 (T013–T017) tras capturarAhora de Dev A
  4. Juntos: Polish (T021–T023)

Notes

  • [P] tasks = different files, no dependencies
  • [Story] label maps task to specific user story for traceability (US1 ↔ FR-001/FR-002/FR-004; US2 ↔ FR-004…FR-007/FR-009; US3 ↔ FR-008/FR-009)
  • Cada historia es independientemente completable y testeable con reloj fijado (30/09/2026 10:00), sin BD salvo regresión RN1/RN2
  • Commit after each task or logical group
  • FR-011: NINGUNA tabla ni endpoint nuevos — si una tarea los necesitara, es alcance fantasma (Constitución IV)
  • FR-010: redefinir «ahora», zona, precisión, bordes o ventanas fuera de lib/(tiempo|ventanas).ts es defecto — el grep T021 lo hace mecánico