Skip to content

feat(api): calculer l'accès d'un parcours depuis les gares d'IDFM - #482

Merged
PaulThiberville merged 2 commits into
mainfrom
issue/180-route-access-idfm
Oct 15, 2026
Merged

PaulThiberville merged 2 commits into
mainfrom
issue/180-route-access-idfm

Conversation

@PaulThiberville

Copy link
Copy Markdown
Collaborator

Résultat et problème

D-050 a retiré le champ saisi « Comment venir » : l'accès d'un parcours se calcule, station la plus proche de la première étape, depuis les données ouvertes d'Île-de-France Mobilités, et se range dans routes.computed. Rien ne le calculait.

  • Source : jeu « Gares et stations du réseau ferré d'Île-de-France (par ligne) » d'IDFM (emplacement-des-gares-idf), Licence Ouverte v2.0 (Etalab), pas ODbL : réutilisation libre, y compris commerciale, sous condition d'attribution (source et date de dernière mise à jour). Mis à jour par IDFM le 2025-12-10. Ce n'est pas un sous-traitant de données personnelles : un fichier public, sans donnée personnelle, importé hors exécution.
  • Import versionné (commit 1) : pnpm --filter @widoo/api transit:import télécharge l'export et la fiche du catalogue depuis deux adresses fixes (ou lit des fichiers avec --file … --meta …), garde métro, RER et tram (940 enregistrements sur 1 240), les regroupe par zone de correspondance (id_ref_zdc) et écrit apps/api/src/transit/generated/stations.json : 767 stations, 108 Ko, une station par ligne pour que le diff d'un rafraîchissement se lise. Le fichier porte sa source, sa licence et sa date. Aucun appel réseau à l'exécution de l'API, pas de table ni de migration : le volume ne l'impose pas.
  • Calcul (commit 2) : station dont un arrêt est le plus proche du lieu de la première étape, toutes ses lignes (métro, puis RER, puis tram, ordre naturel), distance à pied estimée comme un trajet à pied (vol d'oiseau × 1,3, D-088) ; aucune au-delà de 1,5 km (WALK_PROPOSAL_MAX_M). Rangé dans routes.computed.access :
    { "station": "Abbesses", "lines": [{ "mode": "metro", "line": "12" }], "distance_m": 63 }
    ou null. Écrit à chaque enregistrement de l'éditeur (writeInPlace, donc recalculé quand la première étape change) et au seed des dix parcours de démo.
  • Interface : packages/orchestration n'est pas modifié. computeRoute ne prend pas l'accès en entrée et l'éditeur passe par evaluateRoute ; le calcul vit dans apps/api/src/transit et réutilise straightLineM, estimateOfMode et WALK_PROPOSAL_MAX_M.

Accès des parcours de démo : Bastille (1, 5, 8) à 316 m · Abbesses (12) à 63 m · Jacques Bonsergent (5) à 188 m · Corvisart (6) à 506 m · Belleville (2, 11) à 186 m · Jussieu (7, 10) à 262 m · Cité (4) à 231 m · Buttes-Chaumont (7bis) à 537 m · Saint-Sébastien-Froissart (8) à 64 m · Gare de Lyon (1, 14, RER A, D) à 390 m.

Limites : la fiche (RouteDetail.startAccess) n'est pas encore servie par l'API (les fixtures de l'app gardent startAccess: null) et la réponse de l'éditeur (E-11, « Calculé pour vous ») ne porte pas l'accès ; le rafraîchissement est un script à relancer, pas une tâche planifiée ; la validation d'une révision par la modération (PR #365, non fusionnée) devra réécrire computed de la même façon. Signalé sur l'Inbox.

Fixes #180
Écrans : aucun (donnée de E-05 « En pratique » et E-11 « Calculé pour vous », non branchée ici)
Dépendance : aucune

Comment vérifier

  • pnpm turbo run lint typecheck test --force, prettier --check . et pnpm audit --audit-level=high verts en local (tests de l'API relancés seuls : délais dépassés sous la charge de la machine)
  • Testé sur iOS / Android (préciser) — sans objet, aucun code mobile
pnpm --filter @widoo/api exec vitest run src/transit src/db/seed.test.ts src/routes/editor-edit.test.ts
pnpm --filter @widoo/api db:seed   # puis : select computed->'access' from routes;
pnpm --filter @widoo/api transit:import   # rafraîchit le fichier ; le diff passe en PR

Tests : regroupement et ordre des lignes, filtre des modes, nettoyage des caractères invisibles, refus d'un enregistrement hors région, d'un nom avec balise, d'une ligne inattendue, d'une autre licence et d'un export tronqué (moins de 500 stations) ; aller-retour du fichier ; station la plus proche, arrêt le plus proche d'une station à plusieurs arrêts, aucune au-delà de 1,5 km ; accès des dix parcours de démo au seed ; recalcul par PUT /routes/:id/steps quand la première étape change (Bastille, puis Saint-Paul).

Formatage mécanique : non

Risque

Niveau : 2 — nouvelle donnée persistée, non destructive (routes.computed.access), et un import de fichier externe ; ni migration, ni appel sortant à l'exécution.

Taille : commit 1, 325 lignes utiles plus apps/api/src/transit/generated/stations.json (772 lignes, fichier généré par le script d'import, exclu du calcul) ; commit 2, 171 lignes.

Revue OWASP (Top 10 2025), déclenchée par l'import d'un fichier externe :

  • A08 Défaillances d'intégrité des logiciels et des données : le fichier importé est validé enregistrement par enregistrement (zod : zone entière positive, coordonnées dans l'enveloppe de l'Île-de-France, nom en texte brut sans <, > ni caractère de contrôle, 80 caractères au plus, ligne [0-9A-Za-z]{1,5}) ; licence attendue vérifiée, export tronqué refusé (moins de 500 stations), fichier versionné relu en PR et revalidé au chargement de l'API. Testé.
  • A05 Injection : les noms de stations finissent en jsonb par Drizzle (valeurs liées) et en texte React Native ; markup refusé à l'import. Testé.
  • A02 Mauvaise configuration / A01 (SSRF) : le script n'appelle que deux URL https codées en dur, redirect: 'error', délai de 60 s, réponse bornée à 20 Mo ; il tourne sur le poste d'un développeur, jamais dans l'API.
  • A03 Chaîne d'approvisionnement : aucune nouvelle dépendance (fetch et parseArgs de Node).
  • Non applicables : authentification, contrôle d'accès (aucun endpoint nouveau), cryptographie, journalisation.

Licence : la Licence Ouverte demande de mentionner la source et la date de dernière mise à jour là où la donnée est réutilisée ; l'affichage dans l'app est à décider (déposé sur l'Inbox).

Retour arrière

Revert de la PR : computed.access reste dans les lignes déjà écrites, ignoré par tous les lecteurs (la recherche ne lit que la durée, le budget et la distance), et disparaît au prochain enregistrement ou au prochain seed. Aucune migration à annuler.

🤖 Generated with Claude Code

PaulThiberville and others added 2 commits October 15, 2026 13:18
Versioned reference file of the metro, RER and tram stations from the
IDFM dataset emplacement-des-gares-idf (Licence Ouverte v2.0, Etalab),
grouped by connection zone, written by `pnpm --filter @widoo/api
transit:import`. Every record is validated; a changed licence or a
truncated export is refused. No network call at API runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
routes.computed.access holds the station nearest the first step, its
lines and the walk to it, estimated like a walking trip (D-088), none
beyond 1.5 km (D-050). Written at every save of the editor, so it
follows the first step, and at the seed of the demo routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PaulThiberville PaulThiberville added the level:2 Risque niveau 2 (SECURITY.md) label Oct 15, 2026
@PaulThiberville

Copy link
Copy Markdown
Collaborator Author

Plan et décisions techniques :

  1. Fichier versionné plutôt que table : 767 stations, 108 Ko ; un tableau en mémoire et un parcours linéaire (940 arrêts) suffisent à chaque enregistrement. Pas de migration 0015, pas d'appel réseau à l'exécution. Rafraîchir = relancer transit:import et ouvrir une PR avec le diff.
  2. Regroupement par zone de correspondance IDFM (id_ref_zdc) : le nom affiché est celui de l'arrêt le plus proche (les lignes d'une même zone portent parfois des noms différents, « Porte Maillot » et « Neuilly Porte Maillot ») ; les lignes sont toutes celles de la zone. Châtelet et Châtelet-Les Halles restent deux stations, comme chez IDFM.
  3. Modes : métro, RER et tram, comme le ticket. Transilien, Orlyval et câble sont écartés ; à rouvrir si Ilan veut le Transilien.
  4. Distance : à pied estimée (vol d'oiseau × 1,3, comme les trajets enregistrés, D-088), jamais Mapbox ; pas d'accès au-delà de 1,5 km.
  5. Forme stockée : { station, lines: [{ mode, line }], distance_m }, le mode permettant « Métro Abbesses, ligne 12 » ; RouteDetail.startAccess.lines (string[]) n'est pas modifié ici, son branchement suivra l'endpoint de la fiche.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:2 Risque niveau 2 (SECURITY.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calculer l'accès d'un parcours : station la plus proche depuis les données ouvertes d'Île-de-France Mobilités

1 participant