← Tous les projets

Transit : comprendre les données du transport

Le transport public, avec ses données et ses limites.

Un projet personnel d’analyse citoyenne conçu pour plusieurs réseaux de transport, actuellement avec les données de Montréal et d’Ottawa.

Un retard affiché sur une carte semble simple. Pourtant, il faut savoir d’où vient l’information, quand elle a été reçue et ce qu’elle décrit exactement. Est-ce une position signalée? Une prévision de passage? Une observation conservée pour faire un bilan? Ces distinctions changent ce qu’on peut conclure.

Je développe Transit pour rendre ces questions plus faciles à explorer. Le site rassemble des données publiques de transport dans une interface en français et en anglais. On peut regarder la situation d’un réseau, consulter une ligne ou un arrêt, explorer une carte et lire les explications derrière les indicateurs. L’architecture est conçue pour accueillir différents fournisseurs de données de transport. La STM à Montréal et OC Transpo à Ottawa sont les deux réseaux actuellement configurés et présentés dans le site, selon les données disponibles pour chacun.

Transit est un projet indépendant. Son rôle est de rendre le fonctionnement du transport plus lisible et de permettre d’examiner les observations. Pour préparer un déplacement ou vérifier un avis urgent, les services officiels des sociétés de transport restent les références.

Partir d’une question#

L’entrée dans le site part de questions ordinaires : comment va le réseau, quelle ligne regarder de plus près, qu’est-ce qui a été observé au fil des jours? Cette organisation permet de commencer par ce qui nous intéresse, sans avoir à connaître le nom d’un indicateur ni la structure de la base de données.

La carte situe les positions reçues et donne accès aux détails. Les pages de lignes et d’arrêts rapprochent les horaires, les prévisions disponibles et les observations de fiabilité. Les vues de réseau et les bilans quotidiens permettent de prendre du recul. Une page consacrée à la méthode explique ce que les chiffres mesurent, tandis que la santé des données renseigne sur la fraîcheur des sources et les lacunes connues.

Ces différents niveaux de lecture se complètent. Une personne peut commencer par un endroit qu’elle connaît, puis regarder une tendance et revenir à la définition du chiffre. Le détail reste accessible sans devenir un passage obligatoire pour tout le monde.

Deux réseaux, deux contextes#

Montréal et Ottawa rendent particulièrement concret le travail sur les sources. Les deux réseaux publient des horaires et des mises à jour, mais leurs données ne sont pas interchangeables. Changer de ville doit aussi changer le contexte de la carte, les lignes, les arrêts et les liens vers l’organisme concerné.

Ottawa donne un bon exemple de cette exigence. Aucun flux d’avis d’OC Transpo n’est connecté dans Transit. L’interface le dit et propose un lien vers les avis officiels. Afficher zéro avis aurait laissé entendre que le réseau n’avait aucune perturbation, ce que les données ne permettent pas d’affirmer.

Le texte affiché fait partie du code du produit. Dans la version anglaise, ce petit extrait associe le nom du réseau au message et au prochain geste proposé :

code 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 est remplacé par le nom de l’exploitant. Le message distingue un manque d’information d’une absence de perturbations, puis oriente vers la source officielle. Même à cette échelle, écrire l’interface consiste à préciser ce qu’une personne peut raisonnablement comprendre.

Un autre cas concerne les prévisions de passage. Une prévision peut être disponible pour un trajet même si aucun véhicule associé à ce trajet n’apparaît sur la carte. Le travail sur Ottawa a conduit à préserver cette information dans les pages concernées. La présence d’un marqueur et la présence d’une prévision répondent à deux questions différentes.

Un projet qui continue de se construire#

L’interface s’appuie sur une version identifiée de mon socle de design, yesid.dev-design. Les couleurs, les composants partagés et les comportements d’interface ont ainsi une origine commune. Transit garde toutefois ses propres besoins de lecture : une carte dense, des tableaux, des filtres, des états inconnus et des explications qui doivent rester compréhensibles sur un petit écran.

Le projet est déployé et continue d’évoluer. Le site public propose des pages pour Montréal et Ottawa, mais cela ne garantit ni la disponibilité permanente de toutes les sources ni l’exhaustivité de leur historique. Des travaux de réparation et de consolidation sont encore en cours. Les vérifications déjà faites conservent leur portée; elles ne remplacent pas tous les essais sur appareils réels ou avec des technologies d’assistance.

Je développe Transit avec l’appui d’outils d’IA, et je travaille aussi à mieux comprendre le système dans son ensemble. Je prends le temps de suivre le code, de comprendre où chaque comportement est défini et de mieux expliquer les choix du projet. Ce travail d’apprentissage est en cours. Je veux pouvoir relier ce que le site montre à ce qui le produit, puis intervenir avec une compréhension plus solide des conséquences.

Ce projet rassemble plusieurs aspects de mon travail de développeur de solutions numériques à la pige : partir d’une question utile, organiser des sources imparfaites, construire une interface lisible et garder les limites visibles. La suite porte autant sur la fiabilité et la simplicité du système que sur la qualité de ce qu’une personne peut en comprendre.

Explorer Transit

De l’écran aux mécanismes#

La carte est la partie la plus visible de Transit. La suite explique le travail qui permet de relier une prévision, un indicateur et une version publiée. Chaque section se lit séparément et développe un aspect du système, avec les calculs et exemples nécessaires pour le comprendre.

Le dépôt est organisé autour de trois applications qui ont des responsabilités différentes. Cette séparation aide à suivre un problème jusqu’à l’endroit où il peut être expliqué et corrigé.

  • apps/db reçoit les données, les organise dans PostgreSQL et prépare les fichiers publics. Python porte l’ingestion, les traitements, les contrôles et la publication.
  • apps/data-proxy est un Worker Cloudflare qui sert ces fichiers à partir du stockage d’objets. Il s’occupe du transport HTTP : lecture, métadonnées, cache et réponses partielles.
  • apps/web est l’application SvelteKit. Elle consulte les fichiers publiés, valide leur structure et présente les cartes, les tableaux et les explications. Elle n’a pas besoin d’interroger directement la base de traitement.

Prenons une mise à jour fictive pour suivre le parcours complet. Un flux annonce un passage à 14 h 07 à un arrêt, alors que l’horaire correspondant prévoit 14 h 03. Cet exemple est illustratif; il ne décrit pas un trajet observé à Montréal ou à Ottawa.

La première étape consiste à conserver la réponse reçue. La couche appelée Bronze garde la source et son identité. Un condensat permet de reconnaître son contenu, mais le contenu et l’observation ne sont pas la même chose : recevoir deux fois les mêmes octets à deux moments différents peut représenter deux captures. Dédupliquer les fichiers sans conserver cette distinction ferait disparaître une partie de la chronologie.

La couche Silver transforme ensuite la réponse en données relationnelles. Le chargement reçoit la capture exacte et les octets qui viennent d’être recueillis. Il ne redemande pas simplement « la plus récente », qui pourrait avoir changé entre les deux étapes. Le code vérifie que le reçu correspond au fournisseur, au point d’entrée et à l’identifiant de capture attendus.

Dans Gold, les données sont rapprochées et les mesures sont préparées. Pour notre exemple, il faut retrouver le même réseau, la bonne édition de l’horaire, le trajet et la séquence de l’arrêt. Si le rapprochement réussit et que les heures sont exprimées dans le même référentiel, le retard prévu est de 240 secondes. Si l’horaire ne peut pas être relié à la prévision, l’heure de passage peut rester disponible tandis que le retard demeure inconnu.

Les fichiers publiés rendent cette information consultable. Une page peut alors montrer la prévision de passage, son contexte et, lorsqu’il est calculable, le retard associé. L’existence d’une prévision n’exige pas qu’un véhicule correspondant soit visible sur la carte.

Chaque étape garde sa propre trace d’exécution. Une capture peut réussir alors que son chargement échoue; un chargement peut réussir alors que la publication échoue. Ces différences comptent pour reprendre le travail au bon endroit. Un simple voyant vert pour toute la chaîne masquerait le dernier état réellement atteint.

Un identifiant de trajet n’a de sens qu’avec son contexte. Deux fournisseurs peuvent employer la même valeur. Un même fournisseur peut publier une nouvelle édition de son horaire. Une capture contient plusieurs entités, et une mise à jour de trajet contient plusieurs mises à jour d’arrêts. Les clés de rapprochement doivent donc inclure le fournisseur, l’édition concernée, la capture, l’entité et la séquence d’arrêt selon le niveau traité.

Cette précision se retrouve dans le catalogue public. Un réseau peut être actif pour l’ingestion sans être prêt à être offert dans l’interface. Le constructeur du catalogue vérifie notamment l’identité publiée, le fuseau horaire, les limites géographiques et la présence des objets cartographiques attendus. Cet extrait compare trois valeurs du manifeste avec la configuration :

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

Un désaccord arrête la construction du catalogue avec une erreur. Le contrôle ne cherche pas à deviner quelle source serait la bonne. Il empêche de présenter ensemble une identité et une géographie incompatibles.

Cette organisation vise à rendre Transit indépendant d’un fournisseur particulier. Le registre charge les fichiers YAML du dossier de configuration et distingue les réseaux actifs de ceux prêts à être affichés. Chaque manifeste décrit une identité, un fuseau horaire, des limites géographiques, des sources, leur format, leur fréquence et leur mode d’accès. Un horaire GTFS est requis; les positions, les prévisions et les avis sont des entrées facultatives. Déclarer un réseau ne signifie donc pas que toutes les fonctions seront disponibles.

Les traitements communs de collecte et de normalisation reçoivent cette identité; la base et les fichiers publiés conservent le réseau auquel les données appartiennent. Les contrats de publication, le catalogue et l’interface permettent de réutiliser le même parcours de consultation. Certaines sources demandent toutefois une adaptation précise. Le paquet géographique de la STM et ses avis i3 ont leurs propres formats, tandis qu’OC Transpo fournit ses tracés dans son horaire GTFS et utilise un autre en-tête d’authentification pour le temps réel. Ces particularités doivent rester aux endroits qui les traitent.

Ajouter une ville suppose donc de vérifier les sources réelles, leurs conditions de réutilisation et l’attribution, puis de configurer les accès sans publier les secrets. Il faut aussi vérifier que les identifiants des trajets et des arrêts se rapprochent correctement entre l’horaire et les mises à jour. Un format ou une authentification non pris en charge peut demander du code supplémentaire. L’exploitation exige une base préparée, du stockage, des droits d’accès et une collecte planifiée. La configuration seule ne fournit pas ces ressources.

La présence publique vient ensuite : identité publiée cohérente, fond de carte disponible, cadrage adapté, libellés français et anglais, liens officiels et catalogue valide. Les tests du dépôt comprennent un fournisseur synthétique distinct des deux réseaux actuels et des refus de publication lorsque sa carte ou son identité est incohérente. Ils rendent cette extension vérifiable; leur présence ne prouve pas qu’une nouvelle ville a déjà été intégrée ni que ces tests viennent d’être exécutés.

Le stockage reste configuré pour le déploiement, avec des chemins séparés par fournisseur. Le catalogue ne permet pas à chaque contributeur de brancher son propre stockage et ses propres accès sur la même interface hébergée. Cette fédération demanderait une conception supplémentaire. D’autres limites subsistent : des fonctions communes d’affichage du temps utilisent encore America/Toronto, et la configuration d’exploitation par défaut conserve des paramètres et un service de nettoyage propres à la STM. Le point d’accès STM décrit plus loin représente une autre dépendance actuelle.

À terme, je souhaite que d’autres personnes puissent contribuer à ajouter des villes, des sources et les ressources nécessaires pour les exploiter. Ce travail peut porter sur une API, le stockage, les adaptations ou les vérifications. C’est une possibilité d’évolution du projet, pas un service d’inscription automatique.

La géographie a aussi plusieurs fonctions. Une position situe un véhicule signalé. Les formes de l’horaire décrivent des tracés. Un fond de carte fournit le contexte visuel. Les limites et le cadrage déterminent la zone à montrer. Ottawa dispose de formes dans son horaire sans avoir besoin d’un flux géographique séparé pour chacune de ces fonctions. Confondre le fond de carte avec la provenance des positions ferait perdre cette distinction.

Pour le temps, cinq repères sont utiles : la date de service du trajet, l’heure annoncée par le flux, l’heure de mesure d’une position, l’heure de capture par Transit et l’heure de publication du fichier. Une réponse recueillie à midi peut contenir une position mesurée à 11 h 56. La collecte vient d’avoir lieu, mais la position a déjà quatre minutes. Recharger la page ne change pas son âge.

Les horaires de service compliquent encore le rapprochement. Une valeur comme 25:10:00 représente une heure après la fin des premières 24 heures de la journée de service. La ramener simplement à 01:10:00 sans conserver la date de service peut déplacer le trajet au mauvais jour. Le traitement conserve ces heures comme des décalages écoulés et utilise le fuseau du fournisseur. Pour les retards dérivés, le code construit son origine temporelle à partir de midi local moins douze heures afin de traiter les journées de changement d’heure selon cette convention.

Enfin, le choix de la date dépend de la question. Certains bilans regroupent les observations par date locale de capture. D’autres rapprochements ont besoin du jour de service. Ces deux calendriers peuvent diverger après minuit. Donner le nom de la période affichée fait donc partie de la définition d’un indicateur.

La ponctualité de Transit décrit des relevés de retard prévu. Pour les lignes et les bilans quotidiens, une observation est dans la bande « à l’heure » lorsque son retard est d’au moins −60 secondes et de moins de 300 secondes. Le dénominateur comprend les relevés dont le retard est connu.

La formule est : ponctualité (%) = 100 × relevés dans [−60, 300) / relevés à retard connu. Le crochet à gauche inclut −60; la parenthèse à droite exclut 300. Le calcul de publication garde un résultat inconnu si le numérateur manque ou si le dénominateur n’est pas positif :

code 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))

Considérons sept relevés fictifs : −60, 0, 120, 300, 600, 7 200 secondes et une valeur inconnue. Cet ensemble sert uniquement à expliquer les calculs; ce n’est pas une mesure de performance des réseaux.

Six relevés ont un retard connu. Les trois premiers appartiennent à la bande à l’heure. La ponctualité vaut donc 100 × 3 / 6 = 50 %. La valeur inconnue n’est ni un retard nul ni une observation à l’heure. La valeur de 7 200 secondes reste, dans cette mesure, au dénominateur.

Le retard moyen utilise une autre population : les retards connus entre −3 600 et +3 600 secondes, bornes incluses. Dans notre exemple, 7 200 et la valeur inconnue sont exclus. La somme des cinq valeurs restantes vaut 960 secondes; 960 / 5 = 192 secondes, soit 3,2 minutes. Les valeurs extrêmes sont exclues du calcul, pas remplacées par une valeur de 3 600 secondes. Une valeur exclue peut néanmoins correspondre à une perturbation réelle : le filtre définit une population de calcul, il ne certifie pas la source.

La part des retards graves ajoute une troisième distinction. Son numérateur compte les retards strictement supérieurs à 300 secondes et au plus égaux à 3 600. Pour la ligne, il reste un cas grave sur six retards connus, soit 16,7 %. Pour une statistique par arrêt utilisant seulement la plage admissible, il reste un cas sur cinq, soit 20 %. Ce changement ne vient pas d’une nouvelle perturbation; il vient du dénominateur.

Exactement 300 secondes n’est ni dans la bande à l’heure ni dans le numérateur des retards graves. Les deux pourcentages ne doivent donc pas être additionnés en supposant qu’ils couvrent tous les cas.

Il faut aussi regrouper les bons éléments avant de calculer une moyenne. Imaginons un groupe A avec 600 secondes de retard cumulées sur deux observations, et un groupe B avec 300 secondes sur trois observations. Le résultat groupé est 900 / 5 = 180 secondes, soit trois minutes. Faire la moyenne des moyennes de 300 et de 100 secondes donnerait 200 secondes, en accordant le même poids à des groupes de tailles différentes.

C’est pourquoi les résumés conservent des sommes et des comptes, puis convertissent et arrondissent le résultat à la fin. Une ancienne moyenne déjà arrondie ne suffit pas à retrouver une somme exacte. Lorsqu’une heure contributrice nécessaire ne peut pas être reconstruite, le résumé quotidien peut rester indisponible.

La même discipline s’applique aux annulations. Leur taux porte sur les jours-trajets rapportés par le flux, regroupés par identifiant et date de service, dont certains ont été explicitement marqués annulés. Il ne porte pas sur tous les trajets de l’horaire. La comparaison entre volumes observés et programmés reste un autre calcul : un écart de compte ne permet pas de nommer les trajets manquants.

Une moyenne ne décrit pas toute une distribution. Transit utilise aussi des percentiles. La médiane, ou p50, situe le milieu des observations; le p90 décrit une autre position dans leur distribution. Il ne promet pas que le prochain déplacement restera sous cette valeur.

Pour les percentiles quotidiens, les retards connus dans la plage de −3 600 à +3 600 secondes sont triés, puis le calcul interpole entre les valeurs. Avec les quatre valeurs fictives 0, 60, 180 et 600 secondes, la médiane se situe à mi-chemin entre 60 et 180 : 120 secondes. Pour le p90, la position est (4 − 1) × 0,9 = 2,7 en comptant à partir de zéro. On avance de 70 % entre la troisième et la quatrième valeur : 180 + 0,7 × (600 − 180) = 474 secondes, soit 7,9 minutes.

Pour des fenêtres plus longues, les histogrammes regroupés permettent des estimations à partir de classes. Ce n’est pas le même calcul qu’un percentile exact sur toutes les observations brutes. Faire la moyenne de p90 quotidiens ne reconstitue pas non plus le p90 de la période. La période, la population et la méthode doivent rester liées à la valeur présentée.

Les intervalles entre trajets posent une autre question. Le système retient des premières captures admissibles dans une même ligne, direction, journée de service et période horaire. Une capture n’est admissible que si son retard prédit est connu et compris entre −3 600 et +3 600 secondes, bornes incluses. Les séries par fenêtre excluent le service de fin de semaine. Le repère prévu provient de l’horaire GTFS actuel, sur une journée de semaine représentative. Les écarts doivent être positifs et inférieurs à 240 minutes. Il s’agit d’apparitions dans le flux, pas d’arrivées physiquement mesurées à un arrêt. Une collecte interrompue ou un trajet jamais signalé peut modifier l’échantillon.

Le modèle d’attente utilise la somme des écarts et celle de leurs carrés : AWT = somme(écart²) / (2 × somme(écart)), avec les écarts en minutes. Il retranche ensuite la moitié de l’intervalle prévu et ramène un résultat négatif à zéro.

Voici un deuxième exemple illustratif. Deux écarts réguliers de 10 et 10 minutes donnent 200 / 40 = 5 minutes. Deux écarts de 5 et 15 minutes ont la même moyenne de dix minutes, mais donnent 250 / 40 = 6,25 minutes. Avec un repère prévu de dix minutes, l’excès modélisé du second cas est de 1,25 minute, publié à 1,3 minute après arrondi.

Le carré donne davantage de poids aux grands intervalles. Le modèle suppose des arrivées de personnes uniformes pendant ces intervalles, sans données de fréquentation. Il illustre l’effet de l’irrégularité; il ne mesure pas une attente vécue. Les anciennes lignes sans ces moments peuvent utiliser un autre proxy fondé sur la différence des médianes. Le résumé qui fait la moyenne des périodes disponibles n’est pas non plus un calcul regroupant tous les écarts d’une journée.

L’échantillonnage reste déterminant. Si un trajet fournit neuf relevés et un autre un seul, le premier pèse neuf fois plus dans une mesure par observations. Un pourcentage de 90 % peut ainsi décrire neuf prédictions semblables du même trajet. Il ne décrit pas neuf voyageurs sur dix et ne devient pas une proportion de trajets ponctuels.

Les classements utilisent aussi des qualifications d’échantillon et, pour certaines vues, des bornes de Wilson. Le code emploie z = 1,96; certains classements demandent au moins 30 observations admissibles. Ces outils rendent le volume d’observations visible dans le calcul, mais des prédictions répétées ne sont pas des essais indépendants. Ils ne donnent pas une garantie de couverture statistique de 95 % sur le réseau réel. Une donnée absente sur l’occupation, enfin, reste une occupation inconnue, jamais la preuve d’un véhicule vide.

Le site consulte trois familles de données. Le statique décrit notamment les réseaux, lignes, arrêts et objets géographiques. Le temps réel porte les positions, prévisions et états récents. L’historique sert les périodes conservées et les bilans. Ces familles n’ont ni le même rythme de changement ni la même manière d’être rendues visibles.

Les contrats décrivent la forme des fichiers. D’autres métadonnées identifient la méthode et la génération publiée. Une version de structure indique comment lire un document; une version de méthode situe le sens de son calcul; un identifiant de génération associe des résultats à une publication. Confondre ces rôles rendrait une évolution plus difficile à interpréter.

Du côté de PostgreSQL, la publication travaille dans une transaction en isolation REPEATABLE READ. Elle peut lire un état cohérent de la base pendant la préparation. Un verrou consultatif par fournisseur et par famille de publication empêche deux éditeurs de prendre simultanément la même voie. Cela ne transforme pas les écritures en base et les écritures dans le stockage d’objets en une transaction distribuée unique.

Pour le temps réel, les fichiers enfants sont écrits avant le manifeste. Ce dernier sert de repère de fin. Imaginons cinq fichiers à écrire : quatre sont remplacés, puis le cinquième échoue. Le manifeste précédent peut rester en place, mais certaines adresses directes peuvent déjà répondre avec les nouveaux fichiers. « Manifeste écrit en dernier » ne signifie donc pas « tous les fichiers remplacés atomiquement ».

Les vérifications du temps réel sont enregistrées sans bloquer systématiquement sa disponibilité. Pour le statique, le parcours normal vérifie les données avant leur téléversement. Ce sont deux choix de fonctionnement distincts, qui doivent rester visibles lorsqu’on simplifie le code.

L’historique utilise une autre frontière. Les fichiers enfants sont immuables et le graphe qui les référence est préparé avant l’activation de sa racine. La nouvelle racine n’est activée que si la version précédente correspond encore à celle attendue. C’est une écriture conditionnelle : si quelqu’un a changé la racine entre-temps, l’opération rencontre un conflit au lieu d’écraser silencieusement son travail.

Un lecteur qui suit une racine cohérente et ses enfants immuables dispose ainsi d’une génération stable, même si une nouvelle génération est en préparation. Les nouveaux objets non encore activés peuvent exister sans être la version annoncée. Les adresses de compatibilité conservent leur propre comportement; la garantie du graphe ne s’étend pas automatiquement à chaque URL.

Ce compromis permet de servir des fichiers sans imposer les calculs de la base à chaque visite. En retour, il faut gérer la publication, les objets conservés, les échecs et les références actives. La cohérence obtenue dépend de la famille lue et de la façon dont le lecteur suit ses références.

Le Worker de données sert des objets publiés. Il n’effectue pas à chaque requête toute la normalisation décrite plus haut. Cette responsabilité plus étroite permet de traiter séparément les erreurs de données et celles du transport.

Un ETag identifie une version d’objet pour les requêtes conditionnelles. Une requête HEAD peut demander les métadonnées sans recevoir le contenu. Une requête de plage peut récupérer seulement les octets nécessaires d’une archive cartographique. Le code distingue notamment une réponse partielle, une condition non satisfaite, un objet absent et une plage invalide. Une erreur de stockage quelconque ne doit pas être présentée comme une simple mauvaise plage demandée.

Le cache ajoute plusieurs horloges. Il y a la date de l’observation source, celle de la génération publiée, le moment où un résultat intermédiaire a été calculé et le moment où la page le reçoit. Une donnée de 11 h 56 reçue à midi ne devient pas une observation de midi parce qu’une réponse HTTP vient de réussir.

Un cas particulier illustre cette nuance : l’agrégat /api/v1/kpis du code relu est fixé à la STM. Il ne constitue pas le moteur universel des deux villes. Sa réponse peut porter no-store tout en étant construite à partir d’un travail interne mis en cache. Les positions servent de point d’appui à sa disponibilité; l’âge des données de trajets et de réseau qualifie séparément certains champs dérivés. Le cache de travail et la fraîcheur des sources répondent donc à deux questions différentes.

Dans l’application web, le premier affichage peut recevoir des données préparées côté serveur. Le navigateur poursuit ensuite les actualisations et les changements de sélection. Les réponses sont validées à l’exécution; les types TypeScript seuls ne peuvent pas garantir la forme d’un fichier reçu du réseau.

Changer de ville introduit un risque concret : une requête lancée pour Montréal peut se terminer après que la personne a choisi Ottawa. Annuler la première requête aide, mais une réponse tardive peut quand même arriver. La ressource compare un jeton de séquence avant d’accepter le résultat :

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

Si une nouvelle demande a remplacé l’ancienne, son jeton n’est plus courant et la réponse est ignorée. Les données sont aussi associées à une clé de contexte. Une valeur de Montréal n’est pas conservée comme si elle appartenait à Ottawa; une simple actualisation du même contexte peut, elle, garder la dernière valeur utilisable pendant le chargement.

Le catalogue, l’URL, le choix de ville et les liens vers les pages doivent conserver le même fournisseur. Une panne de découverte ne doit pas afficher silencieusement la STM sous une sélection explicite d’Ottawa. Ces règles donnent un sens concret à la cohérence d’une interface : la question choisie et les données visibles doivent continuer de correspondre, même quand les réponses arrivent dans le désordre.

Les vérifications les plus utiles ciblent un comportement qui pourrait raisonnablement se briser. Dans Transit, un test de sélection protège les prévisions de ligne sans véhicule associé. Un test de rendu protège le message d’avis indisponibles. Les tests HTTP vérifient les réponses conditionnelles et les plages. Ils traitent des risques différents.

Les contrats de publication traversent aussi plusieurs langages. Les modèles Python produisent des schémas JSON; le consommateur web utilise ses propres parseurs. Des contrôles de conformité comparent ces représentations. Un fichier généré n’est pas, à lui seul, une seconde spécification indépendante : il faut examiner le producteur, son export et la manière dont le consommateur accepte ou rejette les données.

Pour les transactions, les contraintes et les reprises, des tests sur une vraie base apportent une preuve qu’une simulation ne peut pas fournir. Le dépôt contient notamment des cas où une projection historique échoue à la deuxième étape : les faits, l’état servi et les invalidations doivent revenir ensemble à leur état antérieur. Un autre cas vérifie qu’une relecture historique ne fait pas reculer la version utilisée pour le temps réel.

Cette distinction entre historique et état courant est importante. Corriger un jour passé peut exiger de recalculer ses dérivés, sans vouloir remplacer les positions actuellement servies. Le code garde des identités de captures sélectionnées et des états de traitement. Il faut aussi connaître la politique de rapprochement avec l’horaire : la relecture étudiée utilise l’édition statique courante et rapporte ce choix, plutôt que de prétendre reconstruire exactement toute la connaissance d’une époque.

Les résumés quotidiens gardent leurs propres dates de complétion par famille de mesures. Le fait qu’une famille soit à jour n’établit pas que toutes les autres le sont. Une correction peut invalider les périodes touchées; les sommes exactes, les observations retenues et les dates disponibles déterminent ce qui peut être recalculé.

La conservation est donc un choix fonctionnel autant qu’un choix de stockage. Garder plus de matière première aide à examiner et à rejouer le passé, mais consomme des ressources. Supprimer cette matière réduit le coût et réduit aussi certaines possibilités de reconstruction. Un tableau de bord peut continuer à s’afficher grâce à ses résumés alors que les données nécessaires à une correction plus fine ne sont plus disponibles.

L’exploitation doit pouvoir distinguer un flux absent, une capture en échec, un chargement incomplet, un calcul retardé et une publication qui n’avance plus. Relancer le mauvais étage peut ajouter du travail sans résoudre la cause. Les reçus de traitement, les horodatages et les contrôles de santé servent à localiser cette frontière.

Les sauvegardes suivent la même logique. Un fichier de sauvegarde présent prouve qu’un objet existe. Une restauration testée dans une base temporaire distincte apporte une autre preuve. La remise en service d’une production constitue encore un autre résultat. Le projet possède des procédures séparées pour ces opérations; leur présence dans le dépôt ne signifie pas qu’une restauration complète vient d’être démontrée.

Enfin, les tests de code, les essais sur le site déployé et les vérifications sur appareils réels ou avec des technologies d’assistance restent complémentaires. Ils permettent de décrire ce qui a été vérifié sans transformer un résultat local en promesse générale. C’est aussi une façon de contenir la complexité : conserver les contrôles qui protègent une frontière réelle, et simplifier sans effacer les raisons de leur existence.

Un projet de données de transport conçu pour plusieurs réseaux, avec Montréal et Ottawa comme exemples. Interface, contrats communs, calculs et limites.

2 Réseaux de transport : Montréal et Ottawa
FR / EN Langues de l’interface
PostgreSQLPythonSvelteKitTypeScriptSvelte 5DockerGitHub ActionsPlaywrightVitest
Site en ligne Dépôt privé (pour le moment)

Bâtissons quelque chosequi avance.

Base de données, pipeline, tableau de bord, site web : peu importe où ça accroche, c'est là qu'on commence.