Pilote Orléans · configuration, pas un fork · aucun lieu inventé

Livrable 12

Live, fraîcheur, confiance

13 — Freshness Engine

14 — Trust, conflits, City Pulse

(Un seul document pour que les trois définitions ne divergent pas.)

Principe

« Live » qualifie une fenêtre de validité et un statut de vérification, pas un tuyau websocket. Le transport (poll, SSE) est indépendant. Si le canal n'est pas poussé, on n'écrit pas « temps réel » dans l'UI : on écrit « actualisé à HH:MM ».

Statuts — trois axes

Calcul à la lecture, horloge serveur UTC, timezone ville. On n'affiche jamais un seul badge qui mélange les axes.

AxeValeurs
source_typeBUSINESS_VERIFIED (membre d'un claim approuvé), OFFICIAL_SOURCE, AUTHORIZED_API, CITY_EDITOR, USER_REPORT, OPEN_DATA, PARTNER, UNKNOWN
verification_statusDECLARED_BY_BUSINESS (déclaration, pas un tiers), VERIFIED (last_verified_at dans verified_days), UNVERIFIED
temporal_statusvoir tableau
temporal_statusDéfinition
EXPIREDvalid_until non nul et now >= valid_until, ou event now >= ends_at. Pas un closed_until.
CLOSED_OVERRIDEvenue : closed_until non nul et now < closed_until. L'horaire théorique est ignoré. L'enregistrement du lieu n'expire pas.
LIVEnon expiré, non CLOSED_OVERRIDE, et (fenêtre d'un live_update ou d'une offre), ou event starts_at <= now < ends_at, ou lieu ouvert selon horaires et fraîcheur au-dessus du seuil héros
RECENTnon expiré, updated_at dans recent_hours, hors créneau « maintenant »
STALEnon expiré, last_verified_at plus vieux que stale_after, ou horaire importé plus vieux que hours_stale_after
UNKNOWNpas assez de champs pour trancher. Jamais remplacé par une supposition.

Phrases UI (exemples, pas un score) :

  • « L'établissement indique être ouvert jusqu'à 02:00 · il y a 20 min » = BUSINESS_VERIFIED + DECLARED_BY_BUSINESS + LIVE
  • « Horaire importé · non revérifié depuis … » = OPEN_DATA ou OFFICIAL_SOURCE + UNVERIFIED
  • « Relevé par la rédaction · le … » = CITY_EDITOR + VERIFIED
  • « Fermé jusqu'au … selon l'établissement » = CLOSED_OVERRIDE

USER_REPORT et UNKNOWN ne entrent pas dans Maintenant. Un signalement OPEN n'ajoute pas de badge public. Un badge « signalé » n'existe qu'après une décision RESOLVED qui choisit une mention publique.

EXPIRED sort de Maintenant, Ce soir (si la fin est avant la fenêtre), la carte « en cours », la reco « maintenant ». Il peut rester en archive.

STALE peut rester en liste avec label, pas dans le héros Maintenant.

CLOSED_OVERRIDE exclut le lieu de « ouvert maintenant » et exclut ses événements qui tombent dans la fermeture, sans les marquer EXPIRED.

Freshness — fonction pure

Entier 0–100. Paramètres dans freshness_policy (globale + override ville). Défauts proposés, non validés :

SituationScore
Live update d'un pro approuvé, âge ≤ t_business_high (12 h)100
Même update, âge ≤ stale_after70
Source officielle ou open data licence OK, âge ≤ t_official_high (24 h)80
Même source, âge ≤ t_hours_mid (30 j)60
Horaire importé, âge ≤ t_hours_mid (30 j)55
Horaire importé, âge > 30 j et ≤ t_unverified_low (90 j)40
Non vérifié, âge > 90 j15
EXPIRED0

Le cas « horaire importé de moins de 30 jours = moyenne » est donc 55. Seuil d'entrée du héros Now : hero_freshness_min (50) ou déclaration pro dans t_business_high. Un horaire importé récent peut apparaître, avec la phrase de provenance. Au-delà de 30 jours (score 40) : fiche oui, héros non.

PARTNER et AUTHORIZED_API : ligne créée seulement si un contrat existe. Sinon on ne crée pas la source.

Autres défauts éditables, non validés : stale_after = 30 j, recent_hours = 48 h, verified_days = 30 j. Le City Manager peut les changer ; l'audit enregistre.

Priorité : déclaration d'un claim approuvé > import plus ancien, sans effacer l'historique.

Trust

confidence_score interne, jamais affiché :

  • base par source_type (table) ;
  • + si entity_matches corroborent ;
  • − si signalement non résolu (effet interne seulement) ;
  • − si STALE ;
  • entier plafonné.

UI = phrase de provenance. Pas de « 92 % fiable ».

Conflits

CasGagnant
Import dit ouvert, pro BUSINESS_VERIFIED dit fermé (closed_until ou update)Pro → CLOSED_OVERRIDE ou la déclaration, historique conservé
Deux imports en désaccordhuman_review. Hors héros. La fiche déjà publique reste. La candidate n'est pas publiée.
Signalement closed_businessne ferme pas. Pas de badge public tant que OPEN.
Match sous le seuilpas de fusion auto. merged seulement par acte humain.

match_score : somme pondérée nom (trigram ou ratio), proximité (mètres), téléphone normalisé, host du site. Seuils en politique. Pas de fusion silencieuse au-dessus d'un doute.

City Pulse

Interdit : « 500 personnes », « bondé », heatmap de foule.

Autorisé seulement si le flag city_pulse est on et qu'on affiche la phrase exacte « activité numérique » définie par un comptage :

ouvertures publiées + events actifs + offres actives + publications pro + recherches anonymisées + clics + favoris + demandes d'itinéraire, sur une fenêtre (ex. 60 min), comparée à la médiane de la même zone sur les semaines précédentes s'il y a assez d'historique.

S'il n'y a pas d'historique : on n'affiche pas « forte ». On affiche le compte brut (« 4 événements en cours ») ou rien.

Défaut : flag off.

Now engine

Entrées : now, city_id, optionnel zone_id ou bbox, optionnel origine (lat/lng non stockée).

Inclut si :

  • venue ouverte maintenant selon règles horaires et non closed_until, et pas STALE pour le héros ; ou
  • event actif ou starting_soon ; ou
  • offer / live_update dans la fenêtre.

Exclut EXPIRED, STALE (du héros), CLOSED_OVERRIDE, USER_REPORT, UNKNOWN, human_review, brouillons, alcohol_related si legal_gates.alcohol_ads est off, autres villes. Le plan payant n'est pas un critère.

Rang : voir reco. Distance si origine fournie, sinon pas de distance (pas de centroïde inventé). Si le city manager a publié un centroïde, « distance au centre configuré » est un mode explicite, pas le défaut silencieux.

Cache et jobs

  • Job : passer expired les events/offres/updates dont la fin est dépassée (cohérence des filtres). La lecture filtre aussi sur le temps, pour ne pas dépendre du job.
  • Cible prod : Redis pour la page Now chaude, TTL court (ordre de grandeur : une minute — politique, pas un SLO).
  • Sandbox / Vercel sans Redis : cache HTTP court ou recalcul. Ne pas prétendre un cache distribué.

Décision

  • Choix : statuts définis + fonction pure + exclusion mécanique des expirés.
  • Alternative rejetée : « live = updated_at récent ».
  • Risque : héros vide. C'est préférable à un mensonge.
  • Multi-ville : politiques overridables, définitions identiques.