Files
JWE/docs/architecture.md
T

113 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.md` documente 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](https://openfreemap.org) — 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 `.mbtiles` OpenStreetMap des 4 rĂ©gions (Guadeloupe, Martinique, Guyane, La RĂ©union — quelques dizaines de Mo au total). ProcĂ©dure dans `docs/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)
1. `wbgetentities` sur `www.wikidata.org` → rĂ©cupĂšre le `sitelink` frwiki (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.
2. `GET https://fr.wikipedia.org/api/rest_v1/page/summary/{titre}` → `title`, `extract`, `content_urls`, `thumbnail`.
3. Réponse normalisée : `{ qid, title, extract, url, thumbnail, description }`.
4. 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_id` null cĂŽtĂ© lieu → Cas B « lacune totale » inchangĂ©.
5. **Cache** : `Map` en 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))` avec `r_eff = max(r_difficultĂ©, sqrt(aire_commune_km2 / π))` quand le champ optionnel `aire_commune_km2` est renseignĂ© (A1 — Ă©quitĂ© Guyane : communes immenses), sinon `r_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, `lang` persisté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`. `PhotoPanel` utilise `<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-motion` respecté (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 ; le `LICENSE` racine reste l'historique, un `app/LICENSE` MIT 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Ă©lation `id` round ↔ `id` catalogue (`/api/lieux` n'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 → `radiusEffKm` 37,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 `GameMap` est **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/round` ne 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Ă©ro `Math.random`, dĂ©terminisme inter-machines). `GET /api/defi/[seed]` → 5 rounds sanitisé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) ; composant `CarteRegions.svelte` from scratch (SVG, projection Ă©quirectangulaire par inset, 2×2 responsive, `<button>` par rĂ©gion avec `aria-label` FR/crĂ©ole, SSR sans JS, prop `marqueurs` pour 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é si `prefers-reduced-motion`). Remplace l'ancien « vol » du marqueur vers la position réelle, qui détruisait la comparaison visuelle. Prop `distanceKm` ajoutée à `GameMap` (alimentée par la réponse serveur, jamais calculée cÎté client).