Revisión cruzada de solapamientos — julio 2026

Informe de solo lectura. No modifica ninguna spec. Escrito en el árbol del tronco (main) sin crear ramas ni hacer checkout.

0. Método y procedencia (trazabilidad)

  • Tronco confirmado: git branch --show-current → main.

  • Ramas reales encontradas (git branch --format='%(refname:short)'): analitica-main, main, portal-main, recordatorios-main.

  • Aviso de nomenclatura: el encargo nombraba las ramas 002-portal-del-paciente, 003-recordatorios, 004-panel-analitica. Esos nombres no existen en el repo. La correspondencia real, verificada con git ls-tree -r --name-only REF + git show REF:RUTA desde la punta de cada rama, es:

Spec del encargo Rama real (punta leída) Ruta real en esa rama Commit de punta leído
001 núcleo main tema5/citaclara/specs/001-nucleo-agenda/spec.md a8443c5
Portal del paciente portal-main tema5/citaclara/specs/002-portal-paciente/spec.md 08a1e2b
Recordatorios recordatorios-main tema5/citaclara/specs/002-recordatorios-cita/spec.md 23b5b98
Panel analítica analitica-main tema5/citaclara/specs/002-panel-analitica/spec.md f10d92c
  • Comandos usados (sin checkout, desde main): git show main:tema5/citaclara/specs/001-nucleo-agenda/spec.md, git show portal-main:tema5/citaclara/specs/002-portal-paciente/spec.md, git show recordatorios-main:tema5/citaclara/specs/002-recordatorios-cita/spec.md, git show analitica-main:tema5/citaclara/specs/002-panel-analitica/spec.md.

  • Ninguna rama falta por commit: las cuatro specs son legibles en la punta de su rama. No hubo que parar el análisis.

  • Nota adicional: las tres specs nuevas comparten prefijo 002- (002-portal-paciente, 002-recordatorios-cita, 002-panel-analitica). Colisión de numeración que debe renumerarse antes de fusionar (ver S-09).

Convención de citas: cada cita textual indica entre corchetes la rama de la que se sacó, p. ej. [rama: portal-main].


S-01. Clínica.telefono: el portal lo exige, recordatorios lo prohíbe

Specs implicadas: 001 (main) × portal (portal-main) × recordatorios (recordatorios-main) × analítica (analitica-main).

Citas:

  • Portal, Q7 y FR-016 [rama: portal-main]:

    "¿De dónde sale? → A: se incorpora telefono como campo obligatorio de la entidad Clínica (modelo de datos y semilla de la 001) y el aviso lo muestra. Es una enmienda de la 001, registrada abajo." "Ese campo no existe en el modelo de datos vigente de la 001 y se incorpora como enmienda (ver Supuestos); esta feature MUST NOT arrancar sin él."

  • Portal, Supuestos [rama: portal-main]:

    "Enmienda requerida en la 001 (Principio I): añadir telefono como campo obligatorio de la entidad Clínica —modelo de datos (data-model.md) y semilla («Eleva»)— porque FR-008 y FR-016 de esta spec lo necesitan. La spec 001 y su data-model.md no se han tocado: el cambio requiere el acuerdo de su propietario, Antonio Natusch (ver specs/MAPA.md). Hasta que se aplique, esta feature no puede arrancarse."

  • Recordatorios, Q15 [rama: recordatorios-main]:

    "Q15 — Teléfono de la clínica en el email → A: no se incluye. No se añade campo a la entidad Clínica de la 001; los avisos dicen «llama a la clínica»."

  • Recordatorios, FR-004 [rama: recordatorios-main]:

    "El email MUST NOT incluir el teléfono de la clínica, que la 001 no registra, ni referencia a ningún otro canal de contacto."

  • Recordatorios, Edge [rama: recordatorios-main]:

    "El email no incluye el teléfono (Q15) porque la 001 no lo registra; el aviso es deliberadamente genérico y el paciente ya conoce el contacto de su clínica habitual."

  • 001, Key Entities [rama: main]:

    "Clínica: Consulta que usa CitaClara. Atributos: nombre, clave de panel (auth simplificada v1), jornada configurable (por defecto 09:00–20:00) que delimita la agenda del día. Relación: posee profesionales, servicios, pacientes y citas." (sin telefono).

  • Analítica, Key Entities [rama: analitica-main]:

    "Clínica: Sin cambios respecto a la 001. Solo aporta el ámbito (sus citas) y la clave de acceso."

Por qué chocarán:

  • En spec: dos specs hijas legislan sobre la misma entidad propietaria de la 001 en direcciones opuestas (añadir campo obligatorio vs prohibir añadirlo). Ambas no pueden tener razón a la vez.
  • En código: si el portal añade telefono NOT NULL + semilla, la migración de recordatorios/analítica (que asumen "sin cambios") rompe o queda incompleta; si no se añade, el portal no compila sus avisos FR-008 ("firmeza en el aviso", "campo Clínica.telefono").
  • En producción: avisos incoherentes al paciente ("llama al 910…" vs "llama a la clínica" genérico) según el canal (portal vs email), con el mismo hecho (cancelación fuera de plazo).

Propuesta (un propietario): propietario único = 001-nucleo-agenda (Antonio Natusch) para la entidad Clínica. Decisión recomendada: aceptar la enmienda (añadir telefono obligatorio) porque los dos avisos de fuera de plazo la necesitan para ser accionables, y ordenar a recordatorios retirar su prohibición FR-004/Edge ("MUST NOT incluir… que la 001 no registra") y pasar a "MUST incluir el teléfono de la ficha de la clínica". Hasta el acuerdo, portal y recordatorios quedan bloqueados en este punto (el propio portal ya lo declara).


Specs implicadas: portal (portal-main) × recordatorios (recordatorios-main).

Citas:

  • Portal, FR-003 [rama: portal-main]:

    "El acceso del paciente MUST NOT exigir crear cuentas, altas ni contraseñas, ni gestión alguna por parte de la clínica. El sistema MUST identificar al paciente mediante el correo electrónico y el teléfono que ya figuran en su ficha existente; no se crea ningún dato nuevo para poder entrar."

  • Portal, SC-003 [rama: portal-main]:

    "El 100 % de los intentos de cancelar citas ajenas, pasadas o ya finales se rechaza sin cambiar ningún estado; ningún paciente ve jamás citas de otra ficha. Cero fugas entre fichas en cualquier verificación."

  • Recordatorios, FR-017 [rama: recordatorios-main]:

    "El acceso a la cancelación MUST requerir únicamente el enlace recibido en el email: el sistema MUST NOT exigir contraseña, código ni verificación adicional de la identidad del paciente en esta versión."

  • Recordatorios, Edge [rama: recordatorios-main]:

    "Que el enlace no lleve verificación adicional es una decisión consciente (deuda consciente) recogida en FR-017."

  • Recordatorios, FR-006 [rama: portal-main — corrección: rama recordatorios-main]:

    "Ese enlace MUST llevar a una página de confirmación en español de España que muestre los datos vigentes de la cita y en la que el paciente confirme expresamente la cancelación; solo esa confirmación MUST pasar la cita a cancelada"

Por qué chocarán:

  • En spec: el mismo actor ("el paciente que no llama") tiene dos políticas de autenticación incompatibles: identificación correo+teléfono + sesión firmada de 30 min en el portal, anonimato total con el enlace en el email. La garantía "cero fugas entre fichas" del portal es falsa mientras exista el enlace reenviable de recordatorios.
  • En código: dos caminos de cancelación con distinto middleware de auth sobre la misma transición; el más débil (enlace) domina la seguridad real.
  • En producción: reenviar el .eml permite a un tercero ver datos de la cita (clínica, día, profesional, servicio, precio) y cancelarla, exactamente lo que el portal promete impedir.

Propuesta (un propietario): propietario único = portal-paciente para "identidad del paciente y autorización de cancelación". Recordatorios debe dejar de definir auth propia: su FR-017 pasa a deuda temporal con fecha de caducidad y su enlace debe reutilizar el mecanismo del portal (p. ej. enlace firmado de un solo uso atado a pacienteId, o exigir correo+teléfono antes de confirmar). Mientras tanto, el aviso de riesgo debe aparecer en el propio email, no solo en la spec.


S-03. Cancelación del paciente duplicada: misma transición, dos dueños, dos textos

Specs implicadas: 001 (main) × portal (portal-main) × recordatorios (recordatorios-main).

Citas:

  • 001, FR-011 [rama: main]:

    "El sistema MUST permitir cambiar una cita reservada a completada, a cancelada o a no_asistida, y MUST rechazar cualquier otro cambio de estado (incluido cualquier cambio desde un estado final y cualquier cambio directo entre estados finales)."

  • Portal, FR-007 [rama: portal-main]:

    "Al confirmar, la cita MUST pasar de reservada a cancelada reutilizando exactamente la transición y las garantías de la 001; MUST rechazarse cualquier cancelación sobre citas en estado final o sobre citas de otro paciente. La doble cancelación MUST ser idempotente (la segunda se rechaza como «ya cancelada»)."

  • Portal, FR-008 [rama: portal-main]:

    "El sistema MUST rechazar con aviso claro en español la cancelación desde el portal de toda cita cuyo inicio esté a menos de 24 horas del instante de referencia «ahora», remitiendo al teléfono de la clínica (campo Clínica.telefono, firmeza en el aviso)"

  • Recordatorios, FR-006 + FR-007 [rama: recordatorios-main]:

    "solo esa confirmación MUST pasar la cita a cancelada y liberar el tramo según las reglas de la 001." "Fuera de ese plazo MUST rechazar la cancelación mostrando «fuera de plazo, llama a la clínica» y MUST NOT modificar el estado de la cita."

  • Portal, US2-escenario 2 [rama: portal-main]:

    "el portal la rechaza con un aviso claro en español («Ya no se puede cancelar por internet; llama a la clínica, por favor») y la cita sigue reservada."

Por qué chocarán:

  • En spec: la regla "24 h, borde incluido, precisión de minuto, inicio − ahora" está redactada dos veces con textos de aviso distintos (con vs sin teléfono, ver S-01). Coinciden en el borde pero al estar duplicadas divergirán al primer cambio.
  • En código: dos endpoints/páginas de cancelación (portal + enlace email) implementando "idempotencia ya cancelada", "solo desde reservada", "libera tramo RN1". Riesgo clásico de doble implementación: una olvida el chequeo de "cita de otro paciente" o el lock concurrente.
  • En producción: el paciente que recibe recordatorio y además tiene portal puede cancelar por cualquiera de las dos vías; si una libera y la otra no, o si los mensajes difieren, Sonia recibe la llamada igualmente.

Propuesta (un propietario): propietario único = 001-nucleo-agenda para la transición reservada → cancelada (máquina de estados, idempotencia, liberación RN1, definición de ahora y borde de 24 h). Portal y recordatorios pasan a ser llamadores de un único caso de uso cancelarCita(paciente, citaId, origen) con un único catálogo de avisos. El texto del aviso (con teléfono tras S-01) vive en un solo sitio.


S-04. Estados de lectura "En curso" y "reservada vencida": el portal inventa sub-estados que la analítica ignora

Specs implicadas: 001 (main) × portal (portal-main) × analítica (analitica-main) × recordatorios (recordatorios-main).

Citas:

  • Portal, FR-001 [rama: portal-main]:

    "El sistema MUST ofrecer una página personal por paciente donde, tras identificarse, el paciente ve sus citas separadas en tres secciones: «En curso» (estado reservada con inicio ya pasado y fin todavía futuro), «Próximas citas» (estado reservada con inicio futuro) y «Citas anteriores» (estados completada, cancelada y no_asistida, más cualquier reservada cuyo fin ya sea pasado). «En curso» se muestra arriba de la página […] La clasificación MUST ser exhaustiva y sin solapes: cada cita de la ficha aparece en exactamente una sección."

  • Portal, Edge [rama: portal-main]:

    "Si termina mientras el paciente mira, en la siguiente carga pasa a «Citas anteriores» como reservada con fin pasado."

  • 001, FR-010/FR-011 [rama: main]:

    "Toda cita nueva MUST nacer en estado reservada." "El sistema MUST permitir cambiar una cita reservada a completada, a cancelada o a no_asistida" (sin "en curso", sin "reservada vencida"; toda reservada con fin pasado sigue siendo reservada según la 001).

  • Analítica, FR-002 [rama: analitica-main]:

    "La página MUST mostrar por profesional el porcentaje citas no_asistidas / total de citas de ese profesional en las 8 semanas de historia, contando en el denominador las citas completada, no_asistida y cancelada"

  • Analítica, FR-003 [rama: analitica-main]:

    "Las citas cancelada y no_asistida MUST NOT contar como minutos ocupados (la primera libera el tramo; la segunda no produjo visita)."

  • Recordatorios, FR-001/FR-002 [rama: recordatorios-main]:

    "El sistema MUST ejecutar un proceso diario que seleccione como candidatas todas las citas en estado reservada cuyo inicio caiga dentro de la ventana de 24 a 48 horas" "El sistema MUST excluir del envío toda cita que no esté en estado reservada en el momento de la ejecución"

Por qué chocarán:

  • En spec: el portal crea dos categorías (En curso, reservada con fin pasado en Anteriores) que no existen en la 001 ni en la analítica. La analítica define denominadores cerrados (completada+no_asistida+cancelada) que excluyen a la reservada vencida: esas citas olvidadas (reservada con fin pasado que nadie cerró) desaparecen de tasa, ocupación e ingresos.
  • En código: tres proyecciones temporales distintas sobre el mismo estado + inicio/fin (portal 3 secciones, recordatorios ventana 24–48 h, analítica 8 semanas cerradas). Sin una librería temporal común, cada una calcula "pasado/futuro" a su manera.
  • En producción: una cita olvidada se ve en el portal como "Anterior reservada" pero no cuenta en ningún gráfico de Sara; recepción cree que la cerró y dirección cree que no existió.

Propuesta (un propietario): propietario único = 001-nucleo-agenda para la máquina de estados y el vocabulario temporal (qué es reservada, qué significa fin pasado, si se necesita un job de cierre o se prohíbe la reservada vencida). Portal y analítica solo proyectan: la clasificación exhaustiva del portal (FR-001) debe ascender a regla compartida, y la analítica debe decidir explícitamente dónde cuenta la reservada vencida (recomendado: denunciarla como "cita sin desenlace" en vez de silenciarla, o impedirla con una transición de cierre).


S-05. Precio: referencia viva vs precio congelado vs precio oculto

Specs implicadas: 001 (main) × portal (portal-main) × recordatorios (recordatorios-main) × analítica (analitica-main).

Citas:

  • 001, Key Entities [rama: main]:

    "Relación: cada cita referencia un servicio que determina su duración y su precio de referencia."

  • 001, Edge [rama: main]:

    "Las citas ya creadas conservan su inicio y fin originales; solo las nuevas usan la duración vigente." (congela inicio/fin, no menciona congelar precio).

  • Analítica, FR-004 [rama: analitica-main]:

    "La página MUST mostrar los ingresos por servicio calculados exclusivamente con citas completada como n.º de completadas × precio congelado en la cita al reservarla, con importes exactos al céntimo. El precio de cada cita MUST ser el que tenía su servicio en el momento de la reserva (copia congelada en la cita), NO el precio actual del servicio: cambiar el precio de un servicio MUST NOT alterar los ingresos de las citas ya cerradas ni reescribir un histórico ya mostrado."

  • Analítica, Key Entities [rama: analitica-main]:

    "Ese precio congelado es el único campo que hay que añadir al modelo, para que el histórico de ingresos no cambie cuando la clínica actualiza su tarifa." "Esta feature no crea entidades nuevas; sí añade el campo de precio congelado a Cita (migración de datos, escritura de la reserva, nunca de esta página)."

  • Analítica, FR-006 [rama: analitica-main]:

    "El precio congelado de la cita (FR-004) MUST escribirse al reservar la cita —comportamiento heredado de la 001— y esta feature solo lo lee; su ausencia en este alcance no es una escritura de esta página."

  • Recordatorios, FR-004 [rama: recordatorios-main]:

    "Cada recordatorio MUST incluir […] precio del servicio con el formato exacto de la 001 («40,00 €», céntimos exactos y coma decimal)"

  • Recordatorios, US1-escenario 9 [rama: recordatorios-main]:

    "se genera un nuevo recordatorio con el nombre y el precio del servicio vigente."

  • Portal, FR-013 [rama: portal-main]:

    "En v1 el portal MUST NOT mostrar ningún importe —ni precio de servicio, ni total, ni pendiente— y MUST NOT cobrar."

Por qué chocarán:

  • En spec: la analítica afirma que congelar el precio es "comportamiento heredado de la 001", pero la 001 no lo define (solo congela inicio/fin). Es una enmienda encubierta al modelo, igual que S-01 pero sin declararla como tal.
  • En código: dos fuentes de verdad (precio vivo en Servicio vs copia en Cita) sin especificar backfill de las 409 citas históricas ni qué lee cada lector. Recordatorios dice "precio vigente" y analítica "precio congelado": si la tarifa cambia entre reserva y recordatorio, el email y el gráfico discrepan para la misma cita.
  • En producción: descuadre al céntimo entre lo recordado ("40,00 €"), lo ocultado (portal muestra nada) y lo ingresado (precio congelado distinto). Sara no podrá conciliar.

Propuesta (un propietario): propietario único = 001-nucleo-agenda para "dónde vive el precio de una cita". Decisión recomendada: aceptar el campo cita.precio_congelado_cents (copia en reserva) como enmienda formal de la 001 con migración/backfill, y reescribir recordatorios ("MUST mostrar el precio congelado de la cita, no el vigente") y portal (mantener oculto en v1 pero referenciar la misma fuente para la guarda FR-013). Analítica solo lee.


S-06. Tiempo y ventanas: tres "hoy" y una jornada con denominador dudoso

Specs implicadas: las cuatro.

Citas:

  • 001, Edge + Supuestos [rama: main]:

    "La comparación usa un único instante de referencia del sistema («ahora») documentado en la spec; cualquier inicio estrictamente anterior a «ahora» se considera pasado." "Zona horaria: hora local de la clínica en España (península)"

  • 001, FR-013 [rama: main]:

    "El sistema MUST mostrar la «agenda del día» filtrada por profesional y día dentro de la jornada configurable de la clínica (por defecto 09:00–20:00)"

  • Portal, FR-008 + FR-012 [rama: portal-main]:

    "El límite es de 24 horas exactas: una cita que empieza dentro de 24 h sigue bloqueada, y a las 24 h justas o más ya se puede cancelar." "la antelación de 24 horas se computa con precisión de minuto sobre el instante «ahora» del sistema."

  • Recordatorios, FR-001 + FR-015 [rama: recordatorios-main]:

    "El sistema MUST ejecutar un proceso diario que seleccione como candidatas todas las citas en estado reservada cuyo inicio caiga dentro de la ventana de 24 a 48 horas antes del inicio de la cita, con los bordes incluidos y precisión de minuto, evaluada en hora local de la clínica (Europe/Madrid)." "El sistema MUST ejecutar el proceso diario automáticamente a las 00:00 hora local de la clínica (Europe/Madrid), con horario configurable"

  • Analítica, FR-005 + FR-003 [rama: analitica-main]:

    "la «semana en curso» es la semana natural (lunes a viernes) que contiene la fecha de hoy en Europe/Madrid, y las 8 semanas de historia cerrada son las 8 semanas naturales anteriores (S-8 la más antigua … S-1 la más reciente)." "Con la jornada por defecto 09:00–20:00 y 5 días laborables, la capacidad es 3.300 min/semana y el total de la historia 26.400 min."

Por qué chocarán:

  • En spec: tres definiciones de "hoy" (instante ahora para RN2/24 h, 00:00 para recordatorios, semana natural Lun–Vie para analítica) y dos nombres de zona ("España península" vs "Europe/Madrid") que son lo mismo pero invitan a implementar dos veces. Bordes "incluidos" se repiten en tres specs con tres precisiones.
  • En código: cada feature calculará ventanas por su cuenta; el cambio de hora estacional se trata en 001/recordatorios pero no en analítica (etiquetas S-8…S-1 con rangos fijos de ejemplo que pueden copiarse como constantes).
  • En producción: la ocupación (María 25,1 %, Jorge 18,7 %, Lucía 15,6 % sobre 26.400 min) presupone jornada Lun–Vie 09:00–20:00. Si la clínica abre sábados o reconfigura la jornada (permitido por la 001), el denominador es falso y la ocupación sale inflada sin avisar, aunque la spec diga "MUST indicarse visiblemente la jornada usada".

Propuesta (un propietario): propietario único = 001-nucleo-agenda para tiempo (definición de ahora, zona Europe/Madrid, jornada configurable y calendario laborable). Analítica debe parametrizar el denominador por la jornada vigente real (no asumir 5 días) y recalcular los valores de referencia si la jornada cambia; recordatorios y portal reutilizan el mismo ahora. Unificar el nombre de zona a Europe/Madrid en las cuatro specs.


S-07. Escrituras sobre Cita desde tres features + analítica "que no escribe pero migra"

Specs implicadas: 001 (main) × portal (portal-main) × recordatorios (recordatorios-main) × analítica (analitica-main).

Citas:

  • Analítica, Input + FR-006 [rama: analitica-main]:

    "Solo lectura: esta feature no escribe NADA." "Esta feature MUST NOT escribir nada: no crea, modifica ni elimina citas, profesionales, servicios, pacientes ni clínicas; no expone ningún endpoint de escritura ni botón que mute estado." "Esta feature no crea entidades nuevas; sí añade el campo de precio congelado a Cita (migración de datos, escritura de la reserva, nunca de esta página)."

  • Portal, FR-009 [rama: portal-main]:

    "Toda cita cancelada desde el portal MUST liberar su tramo [inicio, fin), que vuelve a estar disponible para nuevas reservas de recepción sin violar RN1"

  • Recordatorios, FR-003 + FR-010 + FR-011 [rama: recordatorios-main]:

    "Cada envío MUST quedar registrado de forma que una re-ejecución lo detecte." "MUST escribir un fichero .eml por recordatorio en datos/salida-correo/" "La ficha de la cita MUST mostrar una señal legible para recepción con el estado del recordatorio: «Recordatorio enviado el 29/09/2026 a las 00:05» si ya se generó, «Recordatorio pendiente»"

  • 001, FR-007 [rama: main]:

    "El sistema MUST impedir que se registre una cita que solape con otra cita del mismo profesional en estado reservada o completada."

Por qué chocarán:

  • En spec: la analítica se declara "cero escrituras" y a la vez exige migración + nueva escritura en la reserva (precio congelado). Es contradictoria consigo misma y con FR-020 de la 001 ("Quedan fuera de la 001 y MUST NOT implementarse en esta feature: […] analítica o informes").
  • En código: Cita pasa de un escritor (recepción en 001) a cuatro: recepción, portal, enlace de recordatorios, y reserva con precio congelado; más dos tablas/ficheros nuevos (Envío, .eml, informe de incidencias, señal en ficha). Sin un propietario de tabla, los locks de RN1/idempotencia/no-duplicidad (S-03, FR-008 de 001, FR-003 de recordatorios) se implementan tres veces y divergen.
  • En producción: condiciones de carrera "recepción cierra mientras paciente cancela" y "doble ejecución del proceso diario" tocan la misma fila con reglas distintas.

Propuesta (un propietario): propietario único = 001-nucleo-agenda para la tabla Cita y su máquina de estados. Todas las escrituras (incluido el precio congelado de S-05 y la señal de recordatorio) requieren migración aprobada por la 001. Analítica corrige su "solo lectura" a "solo lectura en tiempo de consulta; requiere migración previa propiedad de la 001". Portal y recordatorios no escriben estados directamente: llaman al caso de uso único de la 001 (S-03). La entidad Envío/.eml/informe de incidencias queda como propiedad de recordatorios, con la condición de no mutar Cita salvo por el caso de uso único.


S-08. Métricas de Sara vs estados de Antonio: tasa y ocupación redefinen lo que cuenta

Specs implicadas: 001 (main) × analítica (analitica-main).

Citas:

  • Analítica, Clarifications 29-09 [rama: analitica-main]:

    "Q: ¿Cómo se calcula la «tasa de no asistencia» de cada profesional? → A: no_asistidas entre TODAS sus citas de la historia de 8 semanas, incluidas las canceladas (coherente con el reparto 82/10/8 de la 001)." "Q: ¿Cómo se calcula la «ocupación semanal» de cada profesional? → A: minutos de citas completadas entre minutos de jornada (las canceladas liberan el tramo y las no_asistidas no produjeron visita)."

  • Analítica, FR-002 [rama: analitica-main]:

    "Valores de referencia con la semilla v1: María 11/150 = 7,3 %, Jorge 16/127 = 12,6 %, Lucía 14/132 = 10,6 %, clínica 41/409 = 10,0 %."

  • Analítica, US2 [rama: analitica-main]:

    "Sara ve los ingresos por servicio calculados solo con citas completadas […] esas citas aportan 0,00 € (solo las completadas generan ingreso)."

  • 001, FR-019 [rama: main]:

    "8 semanas de historia pasada con mezcla de completada (82 %), no_asistida (10 %) y cancelada (~8 %), y 2 semanas futuras solo con citas reservada."

  • 001, FR-012 [rama: main]:

    "El estado no_asistida MUST significar exclusivamente «el paciente no se presentó a una cita que seguía reservada»"

Por qué chocarán:

  • En spec: la 001 fija el vocabulario (qué es no_asistida) y el reparto, pero no fija cómo se agregan las métricas. La analítica fija denominadores (tasa incluye canceladas; ocupación e ingresos excluyen canceladas y no_asistidas) que son decisiones comerciales razonables pero nuevas: hacen que la tasa baje cuando hay más cancelaciones, y que una no_asistida cuente como "fracaso" en tasa pero como "vacía" en ocupación. Si mañana recepción cancela en vez de marcar no-show (o viceversa), Sara ve movimientos opuestos en dos gráficos para el mismo hecho.
  • En código: fórmulas con valores absolutos congelados en la spec (409 citas: 335/41/33 + 82 futuras; 14.355,00 €; tablas S-8…S-1) que dependen de SEMILLA_PRNG=1 y de la semana de carga. SC-003 lo matiza ("si la semana cambia, los números MUST cuadrar con la historia realmente cargada"), pero cualquier cambio de semilla o de estados por portal/recordatorios invalida los oráculos sin versionarlos.
  • En producción: Sara "enseña lo que CitaClara ahorra" con una tasa que mezcla cancelaciones (ahorro: hueco reutilizado) con no-shows (pérdida). El argumento comercial queda diluido por la propia fórmula.

Propuesta (un propietario): propietario único = 001-nucleo-agenda para el diccionario de estados; propietario único = analítica para las fórmulas, pero con contrato: la analítica debe publicar sus denominadores como parte versionada del contrato (tasa con canceladas dentro, ocupación/ingresos solo completadas) y la 001 debe acogerlos como anexo, no como redefinición silenciosa. Recomendado: añadir "tasa de no-show pura" (no_asistidas / (completadas + no_asistidas), sin canceladas) junto a la actual para no penalizar la reutilización de huecos, y versionar los oráculos por semilla+ventana en vez de congelarlos.


S-09. Colisiones de identidad de specs: triple 002-, duplicados y normalización

Specs implicadas: portal × recordatorios × 001.

Citas:

  • Portal, Q5 + FR-004 [rama: portal-main]:

    "Si la combinación correo + teléfono coincide con más de una ficha, ¿cómo se desambigua? → A: el acceso se deniega con aviso genérico («Tienes varias fichas con esos datos; llama a la clínica») y se deriva al teléfono de la clínica. Nunca se entra y nunca se enumeran ni distinguen las fichas coincidentes."

  • Portal, Edge [rama: portal-main]:

    "¿Qué pasa si el paciente teclea el correo o el teléfono con mayúsculas, espacios o el prefijo +34? La comparación se normaliza de forma documentada en el plan y una variante equivalente debe dar el mismo resultado que la ficha"

  • Recordatorios, Edge + Q16 [rama: recordatorios-main]:

    "¿Qué pasa con un paciente que ya tiene un recordatorio y al que recepción le cambia el email a uno que usa otra ficha? Cada cita se juzga por su propia configuración recordada: la cita con el email corregido genera su envío nuevo y el histórico se conserva, sin afectar a la otra cita." "Q16 — Cambio del email del paciente en su ficha → A: sí se reenvía. Si recepción corrige el email y la cita sigue en ventana, se genera un recordatorio nuevo al email correcto y el anterior queda como histórico."

  • 001, Clarifications [rama: main]:

    "Q: ¿Qué reglas de unicidad aplican a profesionales, servicios y pacientes en la 001? → A: Solo nombre de servicio único por clínica; resto admite duplicados." "Q: ¿Qué campos son obligatorios y únicos al registrar un paciente como ficha en la 001? → A: Los tres obligatorios y con validación estricta de formato (nombre, teléfono y email siempre exigidos y validados)."

Por qué chocarán:

  • En spec: la 001 admite duplicados de correo/teléfono (solo servicio único). El portal convierte ese duplicado en "denegar acceso", mientras recordatorios lo convierte en "reenviar al email corregido aunque colisione con otra ficha". La misma colisión tiene dos resoluciones opuestas.
  • En código: la normalización (mayúsculas, espacios, +34) solo está definida en el portal ("documentada en el plan"); recordatorios compara "email vigente" para su "configuración recordada" (FR-003) sin decir si normaliza igual. El mismo paciente puede entrar por el portal con una variante y no recibir el recordatorio con otra.
  • En producción: dos fichas gemelas (caso real con duplicados admitidos) dejan a un paciente sin portal (bloqueado) pero con emails duplicados (recordatorios envía a ambos o al incorrecto). A ello se suma la colisión administrativa: tres carpetas 002-* impiden ordenar fusiones.

Propuesta (un propietario): propietario único = 001-nucleo-agenda para unicidad, normalización y fusión de fichas (qué es identidad: pacienteId, no correo+teléfono; cómo se normaliza correo/teléfono; si se permite fusionar duplicados). Portal y recordatorios reutilizan esa normalización. Renumerar carpetas antes de fusionar (p. ej. 002-portal, 003-recordatorios, 004-analitica) y registrar los tres propietarios que faltan en specs/MAPA.md (hoy solo está Antonio Natusch para la 001).


Resumen: quién es dueño de qué (una sola fila por concepto)

Concepto Propietario único propuesto Qué deben hacer las demás
Entidad Clínica y telefono 001 (Antonio Natusch) Portal retira ultimátum; recordatorios retira prohibición e incluye teléfono
Identidad/autorización del paciente Portal-paciente Recordatorios degrada FR-017 a deuda temporal y reutiliza el mecanismo del portal
Transición reservada → cancelada, idempotencia, 24 h, ahora 001 Portal y recordatorios llaman al caso de uso único, sin redefinir
Vocabulario temporal y estados (En curso, reservada vencida) 001 Portal y analítica solo proyectan; definir destino de la vencida
Precio de la cita (referencia vs congelado) 001 (enmienda precio_congelado_cents + backfill) Recordatorios lee congelado; portal mantiene oculto desde la misma fuente; analítica solo lee
Tiempo (ahora, Europe/Madrid, jornada, laborables) 001 Las tres reutilizan; analítica parametriza denominador
Tabla Cita y migraciones 001 Analítica corrige "cero escrituras"; envíos/.eml/incidencias propiedad de recordatorios sin mutar Cita
Fórmulas (tasa, ocupación, ingresos) Analítica (con contrato versionado y anexo en 001) No redefinir estados; añadir tasa pura sin canceladas
Unicidad/normalización/fusión de fichas + numeración 002- 001 + renumeración Portal y recordatorios comparten normalización; completar MAPA.md

Qué no se tocó y qué sigue

  • No se modificó ninguna spec, no se creó ninguna rama, no se hizo checkout: solo se leyó por git show y se escribió este informe.
  • Desbloqueo mínimo sugerido antes de implementar: (1) acuerdo de la 001 sobre telefono y precio_congelado, (2) caso de uso único de cancelación con auth del portal, (3) normalización compartida + renumeración 002/003/004 + propietarios en MAPA.md. Sin eso, portal, recordatorios y analítica divergirán en código aunque hoy coincidan en los bordes.