← Todos los proyectos

Transit: entender los datos de transporte

Transporte público con datos, contexto y límites claros.

Un proyecto personal de análisis ciudadano pensado para distintas redes de transporte, actualmente con datos de Montreal y Ottawa.

Un retraso que aparece en un mapa parece fácil de interpretar. Pero antes conviene entender de dónde viene ese dato, cuándo se recibió y qué representa. Puede ser una posición reportada, una hora estimada de llegada o una observación guardada para un balance diario. Cada caso permite sacar conclusiones diferentes.

Estoy desarrollando Transit para facilitar esa exploración. El sitio reúne datos públicos de transporte en una interfaz en francés e inglés. Permite consultar el estado de una red, revisar una ruta o una parada, explorar un mapa y leer cómo se calculan los indicadores. La arquitectura está pensada para incorporar distintos proveedores de datos de transporte. La STM de Montreal y OC Transpo de Ottawa son las dos redes actualmente configuradas y presentadas en el sitio, según la información disponible para cada una.

Transit es un proyecto independiente. Busca hacer más comprensible el funcionamiento del transporte y permitir que las personas examinen las observaciones. Para planear un viaje o confirmar un aviso urgente, la referencia sigue siendo el servicio oficial de cada empresa de transporte.

Empezar por una pregunta#

El sitio empieza por preguntas cotidianas: cómo está funcionando la red, qué ruta vale la pena revisar con más detalle y qué se ha observado a lo largo de los días. Así se puede entrar por un tema de interés sin conocer de antemano el nombre de un indicador ni la organización de la base de datos.

El mapa muestra las posiciones reportadas y permite consultar sus detalles. Las páginas de rutas y paradas reúnen horarios, estimaciones disponibles y observaciones de confiabilidad. Las vistas de la red y los balances diarios ayudan a mirar el conjunto. Una página de metodología explica qué mide cada cifra, y la sección de estado de los datos muestra qué tan recientes son las fuentes y qué vacíos se conocen.

Estas formas de consultar los datos se complementan. Una persona puede empezar por un lugar que conoce, revisar una tendencia y luego consultar la explicación de una cifra. El detalle queda disponible sin convertirse en un requisito para todo el mundo.

Dos redes, dos contextos#

Trabajar con Montreal y Ottawa hace evidentes las diferencias entre las fuentes. Las dos redes publican horarios y actualizaciones, pero sus datos no son intercambiables. Cambiar de ciudad también debe cambiar el contexto del mapa, las rutas, las paradas y los enlaces a la entidad correspondiente.

Ottawa ofrece un ejemplo concreto. Transit no tiene conectado un flujo de avisos de servicio de OC Transpo. La interfaz lo explica y ofrece un enlace a los avisos oficiales. Mostrar cero avisos daría a entender que no hay interrupciones, algo que los datos disponibles no permiten asegurar.

El texto que aparece en pantalla también forma parte del código. Este fragmento de la versión inglesa relaciona el nombre de la empresa de transporte con el mensaje y el siguiente paso disponible:

código ts
title: 'Service alerts unavailable',
body: (operator: string) =>
	`No ${operator} alert feed is connected here. This does not mean there are no disruptions.`,
link: 'Check official service alerts',

operator se reemplaza por el nombre de la empresa. El mensaje distingue la falta de información de la ausencia de interrupciones y orienta hacia la fuente oficial. Incluso en un detalle pequeño, escribir una interfaz implica pensar en lo que la persona puede concluir al leerla.

Otro caso tiene que ver con las horas estimadas de llegada. Puede existir una estimación para un recorrido aunque el mapa no muestre un vehículo asociado. El trabajo en las páginas de Ottawa permitió conservar esa información donde corresponde. Un marcador de vehículo y una estimación de llegada responden a preguntas distintas.

Un proyecto que sigue en construcción#

La interfaz utiliza una versión identificada de mi base de diseño compartida, yesid.dev-design. Los colores, los componentes comunes y los comportamientos de la interfaz tienen un mismo origen. Transit conserva sus propias necesidades de lectura: un mapa con mucha información, tablas, filtros, estados desconocidos y explicaciones que deben seguir siendo claras en una pantalla pequeña.

El proyecto está desplegado y sigue evolucionando. El sitio público permite consultar ambas redes, pero eso no garantiza la disponibilidad permanente de todas las fuentes ni un historial completo. Continúan los trabajos de reparación y consolidación. Las verificaciones realizadas tienen un alcance específico; no sustituyen todas las pruebas en dispositivos reales o con tecnologías de asistencia.

Estoy desarrollando Transit con el apoyo de herramientas de IA, y también estoy trabajando para comprender mejor el sistema en su conjunto. Dedico tiempo a seguir el código, entender dónde se define cada comportamiento y explicar mejor las decisiones del proyecto. Ese aprendizaje está en curso. Quiero relacionar lo que muestra el sitio con los datos y el código que lo producen, y hacer cambios con una comprensión más sólida de sus consecuencias.

El proyecto reúne varias partes de mi trabajo como desarrollador freelance de soluciones digitales: partir de una pregunta útil, organizar fuentes imperfectas, construir una interfaz comprensible y mantener visibles sus límites. Los siguientes pasos tienen que ver tanto con la confiabilidad y la sencillez del sistema como con la claridad de lo que las personas pueden entender a través de él.

Explorar Transit, disponible en francés e inglés

De la pantalla al funcionamiento del sistema#

El mapa es la parte más visible de Transit. Las siguientes secciones explican el trabajo que conecta una predicción, una medida y una versión publicada. Cada una se puede leer por separado y desarrolla un aspecto del sistema, con los cálculos y ejemplos necesarios para entenderlo.

El repositorio está organizado en tres aplicaciones con responsabilidades distintas. Esa separación ayuda a seguir un problema hasta el lugar donde se puede explicar y corregir.

  • apps/db recibe los datos, los organiza en PostgreSQL y prepara los archivos públicos. Python se encarga de la ingesta, el procesamiento, las verificaciones y la publicación.
  • apps/data-proxy es un Worker de Cloudflare que entrega esos archivos desde el almacenamiento de objetos. Se ocupa del transporte HTTP: lecturas, metadatos, caché y respuestas parciales.
  • apps/web es la aplicación de SvelteKit. Consulta los archivos publicados, valida su estructura y presenta los mapas, las tablas y las explicaciones. No necesita consultar directamente la base donde se procesan los datos.

Pensemos en una actualización ficticia para seguir el recorrido completo. Un flujo estima que un vehículo pasará por una parada a las 2:07 p. m., mientras que el horario correspondiente indica las 2:03 p. m. Es un ejemplo ilustrativo, no un recorrido observado en Montreal u Ottawa.

El primer paso consiste en conservar la respuesta recibida. La capa llamada Bronze guarda la fuente y su identidad. Una huella digital permite reconocer el contenido, pero el contenido y la observación son cosas distintas: recibir los mismos bytes en dos momentos puede representar dos capturas. Eliminar archivos repetidos sin conservar esa diferencia borraría una parte de la secuencia temporal.

La capa Silver convierte la respuesta en registros relacionales. El proceso de carga recibe la captura exacta y los bytes que se acaban de recoger. No vuelve a pedir simplemente «la más reciente», porque podría haber cambiado entre ambos pasos. El código verifica que el comprobante corresponda al proveedor, al punto de entrada y al identificador de captura esperados.

En Gold se relacionan los registros y se preparan las medidas. Para este ejemplo, hay que encontrar la misma red, la edición pertinente del horario, el recorrido y la secuencia de la parada. Si esa relación es válida y las horas usan la misma referencia temporal, el retraso previsto es de 240 segundos. Si no se puede relacionar el horario con la predicción, la hora estimada puede seguir disponible mientras el retraso permanece desconocido.

Los archivos publicados hacen que esa información se pueda consultar. Una página puede mostrar la estimación de llegada, su contexto y, cuando se puede calcular, el retraso asociado. No hace falta que aparezca un vehículo correspondiente en el mapa para que exista una predicción.

Cada etapa conserva su propia evidencia de ejecución. La captura puede funcionar aunque falle la carga; la carga puede funcionar aunque falle la publicación. Esas diferencias importan para retomar el trabajo en el lugar correcto. Un único indicador verde para toda la cadena ocultaría cuál fue la última etapa realmente completada.

Un identificador de recorrido solo tiene sentido dentro de su contexto. Dos proveedores pueden usar el mismo valor. Un proveedor puede publicar una nueva edición de su horario. Una captura contiene varias entidades, y una actualización de recorrido contiene varias actualizaciones de paradas. Por eso, las relaciones deben incluir el proveedor, la edición correspondiente, la captura, la entidad y la secuencia de parada, según el nivel que se esté procesando.

Esa precisión también se aplica al catálogo público. Una red puede estar activa para la ingesta sin estar lista para aparecer en la interfaz. El constructor del catálogo revisa, entre otras cosas, la identidad publicada, la zona horaria, los límites geográficos y la presencia de los objetos cartográficos necesarios. Este fragmento compara tres valores del manifiesto con la configuración:

código python
if (manifest.provider, manifest.tz, manifest.bbox) != (
    provider_id,
    identity.timezone,
    identity.bounds.bbox(),
):
    raise ValueError("Published identity/geography differs from configuration")

Si hay una diferencia, la construcción del catálogo se detiene con un error. La verificación no intenta adivinar cuál fuente es correcta. Evita presentar juntas una identidad y una geografía incompatibles.

Esta organización busca que Transit sea independiente de un proveedor particular. El registro carga los archivos YAML del directorio de configuración y distingue las redes activas de las que están listas para mostrarse. Cada manifiesto describe una identidad, una zona horaria, límites geográficos, fuentes, formatos, frecuencias y formas de acceso. Se exige un horario GTFS; las posiciones, las estimaciones y los avisos son entradas opcionales. Declarar una red no significa que todas las funciones estén disponibles.

Los procesos compartidos de recolección y normalización reciben esa identidad; la base y los archivos publicados conservan la red a la que pertenecen los datos. Los contratos de publicación, el catálogo y la interfaz permiten reutilizar el mismo recorrido de consulta. Algunas fuentes sí requieren una adaptación específica. El paquete geográfico de la STM y sus avisos i3 tienen formatos propios, mientras que OC Transpo incluye los trazados en su horario GTFS y usa otro encabezado de autenticación para los datos en tiempo real. Esas particularidades deben permanecer en los componentes que las procesan.

Agregar una ciudad exige verificar las fuentes reales, sus condiciones de reutilización y la atribución, y configurar los accesos sin publicar los secretos. También hay que comprobar que los identificadores de recorridos y paradas se relacionen correctamente entre el horario y las actualizaciones. Un formato o una autenticación que no se admite puede requerir código adicional. La operación necesita una base preparada, almacenamiento, permisos de acceso y recolección programada. La configuración por sí sola no proporciona esos recursos.

La presentación pública viene después: una identidad publicada coherente, un mapa base disponible, un encuadre apropiado, textos en francés e inglés, enlaces oficiales y un catálogo válido. Las pruebas del repositorio incluyen un proveedor sintético distinto de las dos redes actuales y rechazos de publicación cuando su mapa o su identidad no corresponden. Permiten comprobar esa capacidad de extensión; su existencia no demuestra que ya se haya integrado otra ciudad ni que las pruebas se acaben de ejecutar.

El almacenamiento sigue configurado para el despliegue, con rutas separadas por proveedor. El catálogo no permite que cada persona conecte su propio almacenamiento y sus propios accesos a la misma interfaz alojada. Esa federación necesitaría un diseño adicional. Persisten otros límites: algunas funciones compartidas para mostrar fechas y horas todavía usan America/Toronto, y la configuración operativa predeterminada conserva parámetros y un servicio de limpieza específicos de la STM. El punto de acceso de la STM que se explica más adelante es otra dependencia actual.

A futuro, quiero que otras personas puedan contribuir a agregar ciudades, fuentes y los recursos necesarios para operarlas. Ese trabajo puede incluir una API, almacenamiento, adaptaciones o verificaciones. Es una posibilidad de evolución del proyecto, no un servicio de registro automático.

La información geográfica cumple varias funciones. Una posición ubica un vehículo reportado. Las formas del horario describen los trazados. Un mapa base aporta el contexto visual. Los límites y el encuadre determinan qué zona se muestra. Ottawa incluye formas dentro de su horario, sin necesitar un flujo geográfico separado para cada una de esas funciones. Confundir el mapa base con la procedencia de las posiciones borraría esa diferencia.

En cuanto al tiempo, conviene distinguir cinco referencias: la fecha de servicio del recorrido, la hora indicada por el flujo, el momento en que se midió una posición, la hora en que Transit la capturó y la hora de publicación del archivo. Una respuesta recogida al mediodía puede contener una posición medida a las 11:56 a. m. La recolección acaba de ocurrir, pero la posición ya tiene cuatro minutos. Recargar la página no cambia su antigüedad.

Los horarios de servicio agregan otra dificultad. Un valor como 25:10:00 representa un momento posterior a las primeras 24 horas del día de servicio. Reducirlo a 01:10:00 sin conservar la fecha de servicio puede ubicar el recorrido en el día equivocado. El procesamiento conserva esas horas como desplazamientos de tiempo transcurrido y utiliza la zona horaria del proveedor. Para calcular retrasos derivados, el código construye su origen temporal a partir del mediodía local menos doce horas, de modo que los días de cambio de hora se traten según esa convención.

La fecha apropiada también depende de la pregunta. Algunos balances agrupan las observaciones por fecha local de captura. Otras relaciones necesitan el día de servicio. Esos dos calendarios pueden diferir después de medianoche. Indicar el periodo que se está mostrando forma parte de la definición de un indicador.

La puntualidad de Transit describe observaciones de retrasos previstos. Para las rutas y los balances diarios, una observación está dentro de la franja «a tiempo» cuando su retraso es de al menos −60 segundos y menor que 300 segundos. El denominador contiene las observaciones cuyo retraso se conoce.

La fórmula es puntualidad (%) = 100 × observaciones en [−60, 300) / observaciones con retraso conocido. El corchete de la izquierda incluye −60; el paréntesis de la derecha excluye 300. El cálculo de publicación deja el resultado como desconocido si falta el numerador o si el denominador no es positivo:

código python
def otp_pct(on_time: SqlNumber | None, known: SqlNumber | None) -> int | None:
    if on_time is None or not known:
        return None
    known_obs = float(known)
    if known_obs <= 0:
        return None
    return int(round_half_away(100.0 * float(on_time) / known_obs, 0))

Consideremos siete observaciones ficticias: −60, 0, 120, 300, 600 y 7.200 segundos, más un valor desconocido. Este conjunto sirve únicamente para explicar los cálculos; no describe el desempeño de ninguna de las dos redes.

Seis observaciones tienen un retraso conocido. Las tres primeras pertenecen a la franja a tiempo, de modo que la puntualidad es 100 × 3 / 6 = 50 %. El valor desconocido no representa un retraso de cero ni una observación puntual. El valor de 7.200 segundos permanece en el denominador de esta medida.

El retraso promedio utiliza otra población: retrasos conocidos entre −3.600 y +3.600 segundos, con ambos límites incluidos. En el ejemplo se excluyen 7.200 y el valor desconocido. Los cinco valores restantes suman 960 segundos: 960 / 5 = 192 segundos, es decir, 3,2 minutos. Los valores extremos se excluyen del cálculo; no se reemplazan por 3.600 segundos. Un valor excluido podría corresponder a una interrupción real: el filtro define la población del cálculo, pero no certifica la fuente.

La proporción de retrasos graves introduce una tercera diferencia. Su numerador cuenta retrasos estrictamente mayores que 300 segundos y de hasta 3.600 segundos. Para la ruta, queda un caso grave entre seis retrasos conocidos, equivalente a 16,7 %. Para una estadística por parada que solo usa el rango admisible, queda un caso entre cinco, equivalente a 20 %. La diferencia no se debe a una nueva interrupción, sino al denominador.

Un retraso de exactamente 300 segundos no está dentro de la franja a tiempo ni en el numerador de retrasos graves. Por eso, no se deben sumar ambos porcentajes suponiendo que abarcan todos los casos.

También hay que reunir los elementos correctos antes de calcular un promedio. Supongamos que el grupo A acumula 600 segundos de retraso en dos observaciones, y el grupo B acumula 300 segundos en tres. El resultado conjunto es 900 / 5 = 180 segundos, o tres minutos. Promediar los promedios de 300 y 100 segundos daría 200 segundos, al asignar el mismo peso a grupos de tamaños distintos.

Por eso los resúmenes conservan sumas y conteos, y solo convierten y redondean el resultado al final. Un promedio antiguo que ya fue redondeado no permite recuperar una suma exacta. Si no se puede reconstruir una hora necesaria que contribuye al cálculo, el resumen diario puede quedar no disponible.

La misma disciplina se aplica a las cancelaciones. Su tasa corresponde a días-recorrido reportados por el flujo, agrupados por identificador y fecha de servicio, algunos de los cuales fueron marcados explícitamente como cancelados. No abarca todos los recorridos del horario. Comparar volúmenes observados y programados sigue siendo otro cálculo: una diferencia entre conteos no identifica cuáles recorridos faltan.

Un promedio no describe toda una distribución. Transit también utiliza percentiles. La mediana, o p50, ubica el centro de las observaciones; el p90 describe otra posición dentro de su distribución. No promete que el siguiente desplazamiento vaya a quedar por debajo de ese valor.

Para los percentiles diarios se ordenan los retrasos conocidos entre −3.600 y +3.600 segundos, y el cálculo interpola entre los valores. Con cuatro valores ficticios de 0, 60, 180 y 600 segundos, la mediana queda a mitad de camino entre 60 y 180: 120 segundos. Para el p90, la posición es (4 − 1) × 0,9 = 2,7, contando desde cero. Se avanza un 70 % entre el tercer y el cuarto valor: 180 + 0,7 × (600 − 180) = 474 segundos, o 7,9 minutos.

Para ventanas más largas, los histogramas agrupados permiten hacer estimaciones a partir de rangos de valores. Es distinto de calcular un percentil exacto sobre todas las observaciones originales. Promediar los p90 diarios tampoco reconstruye el p90 de un periodo. El periodo, la población y el método deben mantenerse asociados al valor mostrado.

Los intervalos entre recorridos responden a otra pregunta. El sistema utiliza primeras capturas admisibles dentro de la misma ruta, dirección, jornada de servicio y franja horaria. Una captura solo es admisible si su retraso previsto es conocido y está entre −3.600 y +3.600 segundos, con ambos límites incluidos. Las series por ventana excluyen el servicio de fin de semana. La referencia programada viene del horario GTFS actual, para un día de semana representativo. Los intervalos deben ser positivos y menores que 240 minutos. Son apariciones en el flujo, no llegadas medidas físicamente en una parada. Una interrupción de la recolección o un recorrido que nunca aparece puede cambiar la muestra.

El modelo de espera utiliza la suma de los intervalos y la suma de sus cuadrados: AWT = suma(intervalo²) / (2 × suma(intervalo)), con los intervalos en minutos. Luego resta la mitad del intervalo programado y lleva a cero un resultado negativo.

Un segundo ejemplo ilustrativo permite ver el efecto. Dos intervalos regulares de 10 y 10 minutos dan 200 / 40 = 5 minutos. Dos intervalos de 5 y 15 minutos tienen el mismo promedio de diez minutos, pero dan 250 / 40 = 6,25 minutos. Frente a una referencia programada de diez minutos, el exceso modelado del segundo caso es de 1,25 minutos, publicado como 1,3 minutos después del redondeo.

Elevar al cuadrado los intervalos da más peso a los más largos. El modelo supone llegadas uniformes de personas durante esos intervalos, sin usar datos de cantidad de pasajeros. Ilustra el efecto de la irregularidad, pero no mide la espera que vivió una persona. Los registros antiguos que no tienen esas sumas pueden usar otro indicador aproximado basado en la diferencia entre medianas. Un resumen que promedia las franjas disponibles tampoco equivale a reunir todos los intervalos de un día.

El muestreo sigue siendo decisivo. Si un recorrido aporta nueve observaciones y otro aporta una, el primero pesa nueve veces más en una medida por observaciones. Un porcentaje de 90 % podría describir nueve predicciones parecidas de un mismo recorrido. No describe nueve pasajeros de cada diez ni se convierte en una proporción de recorridos que llegaron a tiempo.

Las clasificaciones también consideran el tamaño de la muestra y, en algunas vistas, límites de Wilson. El código usa z = 1.96; algunas clasificaciones requieren al menos 30 observaciones admisibles. Estas herramientas incorporan el volumen de observaciones al cálculo, pero las predicciones repetidas no son ensayos independientes. No establecen una garantía de cobertura estadística del 95 % para la red real. Por su parte, un dato de ocupación ausente sigue significando ocupación desconocida, no evidencia de un vehículo vacío.

El sitio consulta tres familias de datos. Los estáticos describen, entre otras cosas, redes, rutas, paradas y objetos geográficos. Los datos en tiempo real contienen posiciones, predicciones y estados recientes. El histórico sirve los periodos conservados y los balances. Estas familias cambian a ritmos distintos y se vuelven visibles de maneras diferentes.

Los contratos describen la estructura de los archivos. Otros metadatos identifican la metodología y la generación publicada. Una versión de esquema indica cómo leer un documento; una versión de metodología sitúa el significado del cálculo; un identificador de generación asocia resultados con una publicación. Confundir esas funciones haría más difícil interpretar una evolución del sistema.

En PostgreSQL, la publicación trabaja dentro de una transacción con aislamiento REPEATABLE READ. Puede leer un estado coherente de la base mientras prepara los resultados. Un bloqueo consultivo por proveedor y familia de publicación impide que dos publicadores ocupen simultáneamente esa misma vía. Esto no convierte las escrituras en la base y en el almacenamiento de objetos en una única transacción distribuida.

Para los datos en tiempo real, los archivos hijos se escriben antes que el manifiesto, que sirve como referencia de finalización. Imaginemos cinco archivos por escribir: cuatro se reemplazan y el quinto falla. El manifiesto anterior puede permanecer, pero algunas direcciones directas ya pueden devolver archivos nuevos. Escribir el manifiesto al final no significa reemplazar todos los archivos de manera atómica.

Las verificaciones del tiempo real quedan registradas sin bloquear sistemáticamente su disponibilidad. En el recorrido normal de los datos estáticos, la verificación ocurre antes de subir los archivos. Son políticas de operación diferentes que deben seguir siendo visibles al simplificar el código.

El histórico utiliza otro límite. Los archivos hijos son inmutables y se prepara el conjunto de referencias antes de activar su raíz. La nueva raíz solo se activa si la versión anterior todavía coincide con la esperada. Es una escritura condicional: si otro publicador cambió la raíz mientras tanto, se produce un conflicto en lugar de sobrescribir silenciosamente ese trabajo.

Quien consulta una raíz coherente y sus archivos hijos inmutables puede usar una generación estable mientras se prepara otra. Puede haber objetos nuevos escritos que todavía no pertenecen a la versión anunciada. Las direcciones de compatibilidad conservan su propio comportamiento; la garantía de ese conjunto de referencias no se extiende automáticamente a todas las URL.

Este enfoque permite entregar archivos sin pedirle a la base que repita los cálculos en cada visita. A cambio, el sistema debe gestionar la publicación, los objetos conservados, las fallas y las referencias activas. La coherencia que recibe quien consulta depende de la familia de datos y de cómo se siguen sus referencias.

El Worker de datos entrega objetos publicados. No repite todo el proceso de normalización en cada solicitud. Esa responsabilidad más acotada permite tratar por separado los errores de datos y los del transporte.

Un ETag identifica una versión de un objeto para las solicitudes condicionales. Una solicitud HEAD puede consultar metadatos sin recibir el contenido. Una solicitud por rango puede recuperar solo los bytes necesarios de un archivo cartográfico. El código distingue, entre otros casos, una respuesta parcial, una condición que no se cumple, un objeto ausente y un rango inválido. Un error cualquiera del almacenamiento no debe presentarse como si solo se hubiera pedido un rango incorrecto.

La caché introduce varias referencias de tiempo: cuándo se produjo la observación, cuándo se generó la publicación, cuándo se calculó un resultado intermedio y cuándo lo recibió la página. Una observación de las 11:56 a. m. recibida al mediodía no se convierte en una observación del mediodía porque la solicitud HTTP haya funcionado.

Un caso específico muestra la diferencia. En el código revisado, el agregado /api/v1/kpis está fijado a la STM. No es el servicio general para ambas ciudades. Su respuesta puede indicar no-store y aun así construirse a partir de trabajo interno guardado en caché. Los datos de vehículos sirven como punto de apoyo para su disponibilidad; la antigüedad de los datos de recorridos y de red condiciona por separado algunos campos derivados. La caché del trabajo y la actualidad de las fuentes responden a preguntas distintas.

En la aplicación web, la primera presentación puede recibir datos preparados en el servidor. Después, el navegador continúa las actualizaciones y los cambios de selección. Las respuestas se validan durante la ejecución; los tipos de TypeScript por sí solos no garantizan la estructura de un archivo recibido de la red.

Cambiar de ciudad introduce un riesgo concreto: una solicitud iniciada para Montreal puede terminar después de que la persona elija Ottawa. Cancelar la primera solicitud ayuda, pero una respuesta tardía todavía puede llegar. El recurso compara un identificador de secuencia antes de aceptar el resultado:

código ts
.then((value) => {
	if (token !== seq) return;
	data = value;
	dataKey = activeKey;
})

Si una nueva solicitud reemplazó a la anterior, su identificador ya no es el vigente y la respuesta se ignora. Los datos también están asociados a una clave de contexto. Un valor de Montreal no se conserva como si perteneciera a Ottawa; una actualización del mismo contexto sí puede mantener el último valor utilizable mientras carga.

El catálogo, la URL, la selección de ciudad y los enlaces a las páginas deben conservar el mismo proveedor. Una falla al descubrir el catálogo no debe mostrar silenciosamente datos de la STM bajo una selección explícita de Ottawa. Estas reglas dan un significado práctico a la coherencia de la interfaz: la pregunta elegida y los datos visibles deben seguir correspondiendo, incluso cuando las respuestas llegan en otro orden.

Las verificaciones más útiles se enfocan en un comportamiento que podría fallar de una manera plausible. En Transit, una prueba de selección protege las predicciones de ruta sin vehículo asociado. Una prueba de presentación protege el mensaje de avisos no disponibles. Las pruebas HTTP revisan respuestas condicionales y rangos. Cada una aborda un riesgo diferente.

Los contratos de publicación también cruzan lenguajes. Los modelos de Python producen esquemas JSON, mientras que el consumidor web utiliza sus propios validadores. Las verificaciones de conformidad comparan esas representaciones. Un archivo generado no es, por sí solo, una segunda especificación independiente: hay que revisar el productor, su exportación y la manera en que el consumidor acepta o rechaza los datos.

Para las transacciones, las restricciones y las recuperaciones, las pruebas con una base real aportan evidencia que una simulación no puede ofrecer. El repositorio incluye un caso en el que una proyección histórica falla en su segundo paso: los hechos, el estado servido y las invalidaciones deben regresar juntos al estado anterior. Otro caso verifica que reprocesar datos históricos no haga retroceder la versión utilizada para el tiempo real.

Esa diferencia entre el histórico y el estado actual importa. Corregir un día pasado puede requerir recalcular sus derivados sin reemplazar las posiciones que se están mostrando ahora. El código conserva las identidades de las capturas seleccionadas y los estados de procesamiento. También hay que conocer la política de relación con el horario: el reprocesamiento revisado utiliza la edición estática actual e informa esa decisión, en lugar de afirmar que reconstruye exactamente todo lo que se sabía en una fecha pasada.

Los resúmenes diarios conservan sus propias fechas de finalización por familia de medidas. Que una familia esté actualizada no demuestra que todas las demás lo estén. Una corrección puede invalidar los periodos afectados; las sumas exactas, las observaciones conservadas y las fechas disponibles determinan qué se puede recalcular.

La conservación es una decisión funcional y también de almacenamiento. Guardar más material de origen ayuda a investigar y reprocesar el pasado, pero consume recursos. Eliminarlo reduce el costo y también algunas posibilidades de reconstrucción. Un tablero puede seguir mostrando sus resúmenes aunque los datos necesarios para una corrección más detallada ya no estén disponibles.

La operación necesita distinguir entre un flujo ausente, una captura fallida, una carga incompleta, cálculos retrasados y una publicación que dejó de avanzar. Reiniciar la etapa equivocada puede agregar trabajo sin resolver la causa. Los comprobantes de procesamiento, las marcas de tiempo y las verificaciones de estado ayudan a ubicar ese punto.

Las copias de seguridad siguen la misma lógica. La presencia de un archivo de respaldo demuestra que existe un objeto. Una restauración probada en una base temporal separada aporta otra evidencia. Recuperar la producción es un resultado distinto. El proyecto tiene procedimientos separados para estas operaciones; que estén en el repositorio no significa que se acabe de demostrar una restauración completa.

Por último, las pruebas de código, las verificaciones en el sitio desplegado y los ensayos con dispositivos reales o tecnologías de asistencia se complementan. Permiten describir qué se revisó sin convertir un resultado local en una promesa general. También ayudan a contener la complejidad: conservar las verificaciones que protegen un límite real y simplificar sin borrar la razón por la que existen.

Un proyecto de datos de transporte para distintas redes, con Montreal y Ottawa como ejemplos. Interfaz, contratos compartidos, cálculos y límites.

2 Redes de transporte: Montreal y Ottawa
FR / EN Idiomas de la interfaz
PostgreSQLPythonSvelteKitTypeScriptSvelte 5DockerGitHub ActionsPlaywrightVitest
Sitio en vivo Repo privado (por ahora)

Construyamos algoque se mueva.

Base de datos, pipeline, tablero, sitio web: donde se atasca la cosa, ahí empezamos.