16 KiB
JWE v2 â DĂ©cisions d'architecture (Agent Architecte)
Date : 2026-07-16 · Mission : refonte GeoGuessr souverain (voir projet_geoguessr_97x.md)
1. Décision back-end : SvelteKit seul (pas de Strapi)
Décision : SvelteKit seul, avec les lieux dans un fichier JSON versionné dans Git.
Arguments :
- Frugalité OKI : un seul processus Node à héberger, pas de CMS + admin + DB à maintenir, pas de surface d'attaque supplémentaire. SQLite serait de toute façon peu utile pour un corpus de ~40 lieux statiques.
- CritÚre du prompt (« qui ajoute les lieux ? ») : le corpus est éditorial, produit par l'Agent Contenu et relu. Le workflow Git (pull request = relecture + crédits photos vérifiés) est la modération. Le
docs/contribution.mddocumente l'ajout d'un lieu par PR. - Livrables finaux : le guide de dĂ©ploiement demandĂ© est « SvelteKit + Tile Server » â Strapi n'y figure pas.
- Anti-triche : le JSON reste cÎté serveur (
src/lib/server/), jamais importé par le client ; les endpoints filtrent les champs. Impossible de faire plus simple.
Si un jour des contributeurs non-dev doivent saisir des lieux en masse, la porte vers Strapi reste ouverte (le schéma JSON est le content-type tout désigné), mais ce n'est pas le besoin actuel.
2. Architecture des tiles MapLibre
- Rendu : MapLibre GL JS (verrouillé), chargé en lazy (import dynamique uniquement sur les écrans de jeu / exploration).
- Dev / dĂ©mo : tiles vectorielles OpenFreeMap â libres, basĂ©es OpenStreetMap, sans clĂ© API. Style custom OKI (JSON de style propre, dĂ©rivĂ© du style Positron, re-teintĂ© : mer bleu profond caraĂŻbe, terre sable, vĂ©gĂ©tation verte dense, labels sobres â pas le « bleu Google »).
- Prod auto-hébergée :
tileserver-gl+ extraits.mbtilesOpenStreetMap des 4 rĂ©gions (Guadeloupe, Martinique, Guyane, La RĂ©union â quelques dizaines de Mo au total). ProcĂ©dure dansdocs/deploiement.md. Le style OKI accepte l'URL des tiles par variable d'environnement (PUBLIC_TILES_URL), bascule sans changement de code. - Contrainte gĂ©ographique :
maxBounds= enveloppe union des 4 rĂ©gions (â lon â63âŠ57, lat â23âŠ18) +minZoom, donc impossible de scroller en Europe/AmĂ©rique du Nord. 4 boutons « saut de rĂ©gion » (GUADELOUPE / MARTINIQUE / GUYANE / RĂUNION) replacent la vue sur la rĂ©gion. Limite connue : l'enveloppe rectangulaire inclut l'Atlantique tropical et le BrĂ©sil â inoffensif (aucun indice sur le lieu), documentĂ©. - Attribution © OpenStreetMap contributors affichĂ©e.
3. Stratégie Wikipedia / Wikidata
Principe : rien de stocké en base, interroge l'API en temps réel avec cache court, cÎté serveur uniquement.
Endpoint serveur : GET /api/wiki/[qid] (ex. Q3077840 = Fort DelgrĂšs â l'exemple Q2216838 du prompt initial Ă©tait erronĂ©, corrigĂ© par l'Agent Contenu)
wbgetentitiessurwww.wikidata.orgâ rĂ©cupĂšre lesitelinkfrwiki (et le libellĂ© fr en fallback). Fallback de langue : fr uniquement â dĂ©cision d'audit (A3) : pas de repli sur enwiki, on prĂ©fĂšre l'appel Ă contribution qu'une redirection silencieuse vers l'anglais.GET https://fr.wikipedia.org/api/rest_v1/page/summary/{titre}âtitle,extract,content_urls,thumbnail.- RĂ©ponse normalisĂ©e :
{ qid, title, extract, url, thumbnail, description }. - QID sans sitelink frwiki â
404 {"reason":"no-frwiki"}(cas intermĂ©diaire, A3) : le front affiche la variante Cas B « Pwen blindĂ© â Pa nyen an fransĂ© » (la fiche Wikidata existe, l'article français reste Ă traduire/crĂ©er ; CTA recherche prĂ©-remplie + lien vers la fiche Wikidata).wikidata_idnull cĂŽtĂ© lieu â Cas B « lacune totale » inchangĂ©. - Cache :
Mapen mémoire, TTL 24 h +Cache-Control: max-age=86400. User-Agent explicite (JWE-OKI/x.y (contact)) comme demandé par la politique Wikimédia.
4. Arborescence du projet
JWE/ (dĂ©pĂŽt existant â l'ancien PHP est conservĂ© tel quel)
âââ app/ â nouvelle application SvelteKit 2 / Svelte 5
â âââ src/
â â âââ lib/
â â â âââ server/
â â â â âââ lieux.ts (accĂšs donnĂ©es + sĂ©lection alĂ©atoire + scoring)
â â â â âââ data/lieux.json (corpus 40 lieux â serveur uniquement)
â â â âââ components/ (GameMap, PhotoPanel, ResultPanel, ScoreCardâŠ)
â â â âââ i18n/ (fr.ts, gcf.ts, store)
â â â âââ styles/oki.css (tokens couleurs OKI)
â â â âââ utils/ (score.ts, geo.ts)
â â âââ routes/
â â â âââ +layout.svelte (header, switch langue FR/CR)
â â â âââ +page.svelte (accueil : 4 rĂ©gions, modes, Jouer)
â â â âââ jeu/[mode]/ (Ă©cran de jeu split photo/carte)
â â â âââ explorer/ (mode Aprann : carte pleine page)
â â â âââ api/
â â â âââ round/+server.ts (GET : lieu alĂ©atoire SANITISĂ)
â â â âââ photo/[id]/+server.ts (GET : proxy photo sharp â WebP, sans EXIF, cache 24 h)
â â â âââ guess/+server.ts (POST : {placeId, lat, lon, hints, timeMs} â score + vĂ©ritĂ©)
â â â âââ lieux/+server.ts (GET : catalogue sanitise pour Explorer, SANS id)
â â â âââ wiki/[qid]/+server.ts (GET : extrait Wikipedia, cache 24 h)
â â âââ app.html / app.d.ts
â â âââ service-worker.ts (PWA)
â âââ static/ (manifest.webmanifest, icĂŽnes, photos Ă©ventuelles)
â âââ package.json / svelte.config.js / vite.config.ts
âââ docs/ (architecture, dĂ©ploiement, contribution, audit)
âââ content/ (sources de travail de l'Agent Contenu : crĂ©dits photos)
âââ projet_geoguessr_97x.md
5. Spécifications API REST (anti-triche)
| Endpoint | Méthode | Payload | Réponse |
|---|---|---|---|
/api/round?region=&mode=&exclude= |
GET | â | { id, nom, nom_creole, categorie, difficulte, photo{url,credit,alt,srcset}, indices[] } â jamais coordonnees, commune, region, wikidata_id. photo.url = /api/photo/<uuid> (opaque, proxy serveur â C4) + photo.srcset { "400", "800", "1200" } |
/api/photo/[id]?w= |
GET | w â {400, 800, 1200} (dĂ©faut 1200) |
image/webp retraitée (sharp : rotation auto, métadonnées EXIF/GPS supprimées), Cache-Control: public, max-age=86400, immutable, cache mémoire 24 h. 404 si id inconnu, 400 si w invalide. Fetch amont = URL Commons du corpus uniquement (pas de proxy ouvert) |
/api/guess |
POST | { id, lat, lon, hintsUsed, timeMs? } |
{ distanceKm, score, radiusEffKm, coordonnees, commune, region, nom, wikidata_id } |
/api/lieux |
GET | â | Catalogue sanitise pour Explorer (coords arrondies Ă 0.05°, mode sans score). Sans id (anti-corrĂ©lation round â commune, C4) ; photos en URLs Commons directes (mode sans enjeu) |
/api/wiki/[qid] |
GET | â | { title, extract, url, thumbnail }, Cache-Control: max-age=86400 ; 404 {"reason":"no-frwiki"} si QID sans article frwiki |
RĂšgles :
- Le scoring est recalculé cÎté serveur :
score = round(5000 Ă e^(âd/r_eff))avecr_eff = max(r_difficultĂ©, sqrt(aire_commune_km2 / Ï))quand le champ optionnelaire_commune_km2est renseignĂ© (A1 â Ă©quitĂ© Guyane : communes immenses), sinonr_difficultĂ©seul (50/15/5/1 km). PĂ©nalitĂ© â10 %/indice, bonus temps †+20 % plafonnĂ© Ă 5000.radiusEffKm(0,1 km) est renvoyĂ© pour transparence. - Distance : haversine.
- Le client ne reçoit la vérité (coordonnées, commune, wikidata_id) qu'aprÚs la soumission du marqueur.
exclude: liste d'ids dĂ©jĂ jouĂ©s (mode DĂ©fi 5 rounds, stateless â pas de session serveur nĂ©cessaire, pas de leaderboard).
6. Décisions annexes
- Adapter :
adapter-node(auto-hébergement sobre derriÚre nginx/caddy). - PWA : manifest + service worker minimal (cache app shell, photos en stale-while-revalidate).
- i18n : dictionnaires maison
fr/gcf(créole), store Svelte,langpersistée en localStorage. - Photos : en mode jeu, proxy serveur
/api/photo/[id](C4 â l'URL Wikimedia Commons contient le nom du fichier, souvent un spoiler, et les EXIF peuvent embarquer du GPS) : le serveur fetch l'URL Commons du corpus, retraite avec sharp (rotation auto, suppression de toutes les mĂ©tadonnĂ©es par dĂ©faut, WebP, largeurs 400/800/1200), cache mĂ©moire 24 h +Cache-Control: immutable.PhotoPanelutilise<img src srcset sizes>. L'Explorer (/api/lieux) garde les URLs Commons directes (mode sans enjeu) et le service worker met en cache les deux origines. - A11y / calm tech :
prefers-reduced-motionrespectĂ© (pas de vol de marqueur), timer informatif non anxiogĂšne, navigation clavier (carte : flĂšches dĂ©placent le marqueur, EntrĂ©e valide). - Licence code : MIT (le dĂ©pĂŽt historique Ă©tait AGPL â la refonte est un nouveau module
app/sous MIT comme exigé par le prompt ; leLICENSEracine reste l'historique, unapp/LICENSEMIT est ajouté).
7. Correctifs d'audit (2026-07-17)
- C4 â URLs photo spoiler : voir §6 « Photos » et §5. Deux fuites colmatĂ©es : le nom de fichier Commons dans
photo.url(remplacĂ© par le proxy opaque/api/photo/<uuid>) et la corrĂ©lationidround âidcatalogue (/api/lieuxn'expose plus d'id; l'Explorer utilise des indices Ă©phĂ©mĂšres cĂŽtĂ© client). - A1 â Radius de score en Guyane : voir §5 « RĂšgles ».
aire_commune_km2(champ optionnel du schĂ©ma) Ă©largit le rayon effectif :sqrt(aire/Ï). Test de rĂ©fĂ©rence : Saint-Laurent-du-Maroni (4320,8 kmÂČ, EXPERT), guess Ă 40 km âradiusEffKm37,1, score â 1700 (au lieu de â 0). Le code tolĂšre l'absence du champ. - A2 â Pas de fuite de rĂ©gion : la camĂ©ra initiale de
GameMapest globale (center: [-58, 0],zoom: 3.6â vue d'ensemble des 4 rĂ©gions, aucun indice sur le lieu). Les 4 boutons rĂ©gion sont la devinette en deux temps : Ă©tape 1 = sauter vers la rĂ©gion supposĂ©e, Ă©tape 2 = poser le marqueur. Les deux Ă©tapes sont combinĂ©es par la distance seule : mauvaise rĂ©gion â distance Ă©norme â score â 0. Aucune donnĂ©e serveur ne rĂ©vĂšle la rĂ©gion avant le guess (/api/roundne l'inclut pas). - A3 â ChaĂźne Wikipedia : voir §3.
Cache-Control: max-age=86400; cas intermĂ©diaire QID sans frwiki â404 {"reason":"no-frwiki"}â variante Cas B dĂ©diĂ©e cĂŽtĂ© front ; fallback de langue fr uniquement, documentĂ© dans le code.
8. Phase 2 (2026-07-17) â ADR-001 v1 et ADR-002
- ADR-001 v1 â DĂ©fi par lien Ă seed (
/defi/<seed>) :src/lib/server/tirage.ts(hash cyrb53 â PRNG mulberry32, Fisher-Yates dĂ©terministe, contraintes â„1 MONUMENT / â„1 LIEU / â„2 rĂ©gions par Ă©changes dĂ©terministes â zĂ©roMath.random, dĂ©terminisme inter-machines).GET /api/defi/[seed]â 5 rounds sanitiseÌs identiques pour tous (no-store, seed alphanumĂ©rique 4â40). Accueil : « CrĂ©er un lien de dĂ©fi » (seed 8 chars sans ambiguĂŻtĂ©). Bilan : « Copier le lien du dĂ©fi ». La comparaison des scores reste humaine (image canvas) â zĂ©ro compte, zĂ©ro Ă©tat serveur, zĂ©ro leaderboard. La v2 temps rĂ©el (PlaySocketJS) reste conditionnĂ©e au gate de traction de l'ADR (â„30 parties/sem. Ă4 sem. + â„5 demandes explicites). - ADR-002 â Carte SVG lĂ©gĂšre hors-jeu : pipeline
app/scripts/build-carte-regions.mjs(geo.api.gouv.fr â mapshaper 7 % âsrc/lib/map/regions-geo.json, 91,8 KB gz †150 KB) ; composantCarteRegions.sveltefrom scratch (SVG, projection Ă©quirectangulaire par inset, 2Ă2 responsive,<button>par rĂ©gion avecaria-labelFR/crĂ©ole, SSR sans JS, propmarqueurspour le rĂ©cap guessârĂ©el, attribution © OSM/donnĂ©es publiques). UtilisĂ©e sur l'accueil (sĂ©lection de rĂ©gion) et les bilans dĂ©fi â MapLibre reste strictement aux Ă©crans de jeu (devinette, rĂ©sultat, explorer). Attribution : contours © geo.api.gouv.fr / donnĂ©es publiques.
9. Correctif UX résultat (2026-07-17)
- Carte résultat façon GeoGuessr : à la révélation, le marqueur du joueur reste à sa position (corail) et la position réelle apparaßt (vert), reliées par une ligne pointillée avec étiquette de distance au milieu (
distanceKm) ; la carte cadre les deux points (fitBounds, instantanĂ© siprefers-reduced-motion). Remplace l'ancien « vol » du marqueur vers la position rĂ©elle, qui dĂ©truisait la comparaison visuelle. PropdistanceKmajoutĂ©e ĂGameMap(alimentĂ©e par la rĂ©ponse serveur, jamais calculĂ©e cĂŽtĂ© client).
10. ADR-003 â Mode street « An lari a » via Mapillary (2026-07-18)
5e mode de jeu : le joueur voit une photo street-level navigable (visionneuse Mapillary) au lieu d'une photo statique, et devine la commune. Mécanique carte + guess + score inchangée.
- Pourquoi Mapillary : seule source de street-level imagery couvrant correctement les 4 régions (bonne couverture Gwada/Martinique/Réunion, littoral guyanais) ; images CC BY-SA 4.0, lib
mapillary-jsMIT, API gratuite (token client requis, rate-limité par app). Exception documentée à « zéro service tiers » (Mapillary = plateforme Meta), limitée à ce mode ; alternative souveraine future = Panoramax (reco R4 audit passe 2), couverture DOM encore insuffisante à cette date. - Corpus curé, pas de tirage aléatoire dans l'API :
app/src/lib/server/data/street.json(20 lieux, 5/rĂ©gion), produit parapp/scripts/curate-street.mjs(bbox < 0,01° ou grille de recherches par rayon autour de la mairie â le centroĂŻde geo.api tombe souvent hors du centre-ville ; tri qualitĂ©/rĂ©cence, prĂ©fĂ©rence 360°), revĂ©rifiĂ© parapp/scripts/validate-street.mjs(image existe,computed_geometry< 100 m, crĂ©dit auteur, sitelink frwiki). La vĂ©ritĂ© terrain (coordonnees) est figĂ©e dans le corpus (dĂ©rive possible decomputed_geometrycĂŽtĂ© Mapillary). - Phase Ă©ducative par commune :
wikidata_id= QID de la commune (un coin de rue n'a pas vocation Ă avoir sa page) âWikiFicherĂ©utilisĂ© tel quel, aucune variante Cas B nouvelle.aire_commune_km2renseignĂ© (Ă©quitĂ© Guyane, A1). - Anti-triche :
/api/round?mode=street+sanitizeRoundStreet()â seulsid, nom, nom_creole, categorie, difficulte, mapillaryId, credit, indicespartent ; coords/commune/rĂ©gion/wikidata_id jamais avant le guess. Limitation assumĂ©e : l'image_keyest publique (la visionneuse appelle Mapillary depuis le navigateur) â un joueur techniquement Ă©quipĂ© peut retrouver la position via l'API Mapillary (mĂȘme propriĂ©tĂ© que GeoGuessr/Street View ; acceptable en solo sans leaderboard). La visionneuse n'affiche pas de carte (mapillary-js n'en a pas par dĂ©faut), donc aucun spoiler visuel. - Visionneuse :
StreetPanel.svelte, lazy load façon MapLibre (import('mapillary-js')+ CSS â chunk sĂ©parĂ© ~1 MB, 0 occurrence dans le bundle initial),component.cover: false,moveTo()avec Ă©tat d'erreur (image retirĂ©e â message + round suivant possible). TokenPUBLIC_MAPILLARY_TOKENvia$env/dynamic/public(patternPUBLIC_TILES_URL) : token client, conçu par Mapillary pour ĂȘtre exposĂ© cĂŽtĂ© navigateur. - RĂ©utilisation sans modification :
GameMap,ResultPanel,WikiFiche, scoring (scoreGuess/skipResultacceptentLieu | StreetLieu), timer, indices, bouton « Passer ».RoundPlace.photodevient optionnel (mapillaryId/creditajoutés) ; lookup unifiégetLieuById(2 corpus) pour/api/guess,getLieuPhotoByIdconservé pour le défi et le proxy photo. - Hors scope v1 : street dans
/defi/[seed]et/explorer, cache SW des tuiles Mapillary (les requĂȘtesgraph.mapillary.com/FBCDN passent en direct).