3 Commits

7 changed files with 40 additions and 221 deletions
-1
View File
@@ -33,7 +33,6 @@ npm run build && node build # production (adapter-node)
- [`docs/deploiement.md`](docs/deploiement.md) — auto-hébergement (SvelteKit + tileserver-gl). - [`docs/deploiement.md`](docs/deploiement.md) — auto-hébergement (SvelteKit + tileserver-gl).
- [`docs/contribution.md`](docs/contribution.md) — ajouter un lieu et lier sa fiche Wikidata. - [`docs/contribution.md`](docs/contribution.md) — ajouter un lieu et lier sa fiche Wikidata.
- [`docs/audit-oki.md`](docs/audit-oki.md) — audit qualité et doctrine OKI. - [`docs/audit-oki.md`](docs/audit-oki.md) — audit qualité et doctrine OKI.
- [`prompts/`](prompts/) — prompts de mission et d'audit ayant produit cette refonte.
## Contributions ## Contributions
+34 -27
View File
@@ -23,6 +23,7 @@
interface Props { interface Props {
guess?: Coordonnees | null; guess?: Coordonnees | null;
real?: Coordonnees | null; real?: Coordonnees | null;
distanceKm?: number | null;
interactive?: boolean; interactive?: boolean;
pairs?: Paire[]; pairs?: Paire[];
markers?: MarqueurInfo[]; markers?: MarqueurInfo[];
@@ -34,6 +35,7 @@
let { let {
guess = $bindable(null), guess = $bindable(null),
real = null, real = null,
distanceKm = null,
interactive = true, interactive = true,
pairs = [], pairs = [],
markers = [], markers = [],
@@ -48,7 +50,6 @@
let guessMarker: Marker | null = null; let guessMarker: Marker | null = null;
let realMarker: Marker | null = null; let realMarker: Marker | null = null;
let explorerMarkers: Marker[] = []; let explorerMarkers: Marker[] = [];
let raf = 0;
const SRC_LIGNE = 'jwe-ligne'; const SRC_LIGNE = 'jwe-ligne';
const SRC_PAIRES = 'jwe-paires'; const SRC_PAIRES = 'jwe-paires';
@@ -100,39 +101,47 @@
id: 'jwe-ligne-line', id: 'jwe-ligne-line',
type: 'line', type: 'line',
source: SRC_LIGNE, source: SRC_LIGNE,
filter: ['==', ['geometry-type'], 'LineString'],
paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] } paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] }
}); });
map.addLayer({
id: 'jwe-ligne-label',
type: 'symbol',
source: SRC_LIGNE,
filter: ['==', ['geometry-type'], 'Point'],
layout: {
'text-field': ['get', 'etiquette'],
'text-size': 14,
'text-font': ['Noto Sans Regular'],
'text-offset': [0, -1.4]
},
paint: { 'text-color': '#0b3d2e', 'text-halo-color': '#fffdf8', 'text-halo-width': 2 }
});
}
const features: GeoJSON.Feature[] = [];
if (guess) {
features.push(ligneFeature(guess, real, null));
if (distanceKm !== null) {
// Étiquette de distance au milieu de la ligne guess ↔ réel
features.push(
pointFeature(
{ lat: (guess.lat + real.lat) / 2, lon: (guess.lon + real.lon) / 2 },
{ etiquette: `${distanceKm} km` }
)
);
}
} }
const features: GeoJSON.Feature[] = guess ? [ligneFeature(guess, real, null)] : [];
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features }); setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features });
// Marqueur vert = position réelle // Marqueur vert = position réelle ; le marqueur corail du joueur RESTE
// à sa position : la comparaison visuelle guess ↔ réel est le cœur du jeu.
realMarker?.remove(); realMarker?.remove();
realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map); realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map);
// Le marqueur du joueur « vole » vers la position réelle (sauf reduced-motion) if (guess) {
if (guessMarker && guess) {
const from = { ...guess };
if (reducedMotion) {
guessMarker.setLngLat([real.lon, real.lat]);
} else {
const debut = performance.now();
const duree = 1400;
const step = (now: number) => {
if (!guessMarker) return;
const k = Math.min(1, (now - debut) / duree);
const e = 1 - Math.pow(1 - k, 3);
guessMarker.setLngLat([
from.lon + (real.lon - from.lon) * e,
from.lat + (real.lat - from.lat) * e
]);
if (k < 1) raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
}
const bounds = new ml.LngLatBounds( const bounds = new ml.LngLatBounds(
[Math.min(from.lon, real.lon), Math.min(from.lat, real.lat)], [Math.min(guess.lon, real.lon), Math.min(guess.lat, real.lat)],
[Math.max(from.lon, real.lon), Math.max(from.lat, real.lat)] [Math.max(guess.lon, real.lon), Math.max(guess.lat, real.lat)]
); );
map.fitBounds(bounds, { padding: 80, duration: reducedMotion ? 0 : 900, maxZoom: 12 }); map.fitBounds(bounds, { padding: 80, duration: reducedMotion ? 0 : 900, maxZoom: 12 });
} else { } else {
@@ -141,7 +150,6 @@
} }
function nettoyerRevelation(): void { function nettoyerRevelation(): void {
cancelAnimationFrame(raf);
realMarker?.remove(); realMarker?.remove();
realMarker = null; realMarker = null;
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] }); setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
@@ -311,7 +319,6 @@
})(); })();
return () => { return () => {
detruit = true; detruit = true;
cancelAnimationFrame(raf);
map?.remove(); map?.remove();
map = null; map = null;
}; };
+1
View File
@@ -266,6 +266,7 @@
<GameMap <GameMap
bind:guess bind:guess
real={result ? result.coordonnees : null} real={result ? result.coordonnees : null}
distanceKm={result ? result.distanceKm : null}
interactive={!result} interactive={!result}
onvalidate={() => void soumettre(false)} onvalidate={() => void soumettre(false)}
/> />
+1
View File
@@ -260,6 +260,7 @@
<GameMap <GameMap
bind:guess bind:guess
real={result ? result.coordonnees : null} real={result ? result.coordonnees : null}
distanceKm={result ? result.distanceKm : null}
interactive={!result} interactive={!result}
onvalidate={() => void soumettre(false)} onvalidate={() => void soumettre(false)}
/> />
+4
View File
@@ -106,3 +106,7 @@ Règles :
- **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 440). 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-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 440). 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. - **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).
@@ -1,112 +0,0 @@
# JWE — ADR-001 & ADR-002 (format audit OKI)
Deux décisions d'architecture rédigées selon le format du prompt d'audit post-exécution : critères bloquants, preuves exigées, rapport tabulaire, interdits. Elles **ne modifient pas** le build v1 en cours ; elles cadrent la phase 2 et une optimisation perf.
---
# ADR-001 — Mode Défi : seed-link en v1, PlaySocketJS en phase 2 conditionnée
- **Statut** : Accepté (v1) / À étudier (v2, gate explicite)
- **Date** : 2026-07-17
- **Décideurs** : Architecte OKI
- **Références** : PlaySocketJS (MIT, rooms, CRDT, server-authoritative, battle-tested >1,3 M duels sur 1 instance Node) ; Geoguess/Geoguess (modèle de room sans compte, max 5 joueurs) ; prompt v2 §A5 (pas de comptes, pas de leaderboard serveur).
## Contexte
Le prompt v2 interdit comptes et leaderboard serveur. L'usage réel visé (OKI, AKILPA, salles de classe, événements) crée pourtant un besoin : **jouer les mêmes 5 lieux que d'autres et comparer les scores**. Deux horizons : maintenant (zéro serveur) et plus tard (temps réel, si le projet prouve sa traction).
## Décision
**v1 — Défi asynchrone par lien à seed partagée.**
- L'URL de défi contient une seed opaque (ex. `/defi/<seed>`) ; le serveur (ou la fonction SvelteKit) dérive de la seed les mêmes 5 lieux pour tous les joueurs (PRNG seedé côté serveur, jamais côté client).
- Aucune coordonnée dans l'URL ni dans le payload initial (anti-triche §6 du prompt v2 maintenu).
- Fin de partie : l'image de partage canvas (déjà prévue prompt v2 §4.4) affiche le score ; la comparaison se fait humainement (screenshot dans le groupe WhatsApp/Signal). Zéro serveur d'état, zéro compte, cohérent doctrine.
**v2 — Duel temps réel via PlaySocketJS, UNIQUEMENT si le gate §"Conditions" est franchi.**
- Modèle : rooms sans compte à la Geoguess (nom de room partagé, 25 joueurs), état synchronisé par PlaySocketJS en mode **server-authoritative** (le serveur valide les guesses et calcule les scores — jamais le client).
- Hébergement : 1 instance Node sur le VPS OKI existant, dans l'enveloppe 4050 €/mois. Aucune dépendance Google/Firebase (contraire à Geoguess — voir annexe).
## Options considérées
| Option | Souveraineté | Coût serveur | Effort | Anti-triche | Verdict |
|---|---|---|---|---|---|
| Seed-link asynchrone (v1) | Totale | Nul | S | Serveur dérive les lieux, scores non comparés en ligne | **Retenu v1** |
| PlaySocketJS rooms (v2) | Bonne (MIT, self-host) | 1 instance Node | M | Server-authoritative natif | **Retenu v2 si gate** |
| Socket.io maison | Bonne | 1 instance Node | ML | À réimplémenter | Rejeté (réinvention) |
| Supabase/Firebase Realtime | Mauvaise (cloud propriétaire) | Abonnement | M | Faible côté client | Rejeté (doctrine) |
## Conditions de déclenchement de la v2 (gate — bloquant)
La v2 n'est étudiée que si TOUTES les conditions suivantes sont remplies et documentées dans le rapport de gate :
1. Traction mesurée : ≥ 30 parties/semaine pendant 4 semaines consécutives (mesure locale anonymisée, pas d'analytics tiers).
2. Demande explicite : ≥ 5 retours d'utilisateurs ou partenaires (OKI, AKILPA, enseignants) demandant le défi en direct.
3. Capacité d'hébergement confirmée sur le VPS OKI sans dépasser l'enveloppe 4050 €/mois.
## Vérifications (obligatoires, preuves à l'appui)
- **V1 (bloquant, v1)** — Déterminisme : la même URL de défi produit les mêmes 5 lieux, dans le même ordre, sur deux machines différentes. Preuve : double exécution filmée ou logs serveur.
- **V2 (bloquant, v1)** — Anti-fuite : l'URL et les payloads initiaux ne contiennent ni coordonnées, ni commune, ni `wikidata_id` décodable. Preuve : inspection réseau + revue de l'URL.
- **V3 (bloquant, v1)** — L'image de partage n'affiche que score/pseudo libre, jamais les réponses des autres joueurs.
- **V4 (bloquant, v2)** — POC PlaySocketJS : room à 3 clients, déconnexion/reconnexion d'un joueur sans perte d'état (CRDT), score validé côté serveur (tentative de triche client rejetée — preuve par requête forgée).
- **V5 (v2)** — Licence : `npm view playsocketjs license` = MIT, consigné dans le rapport.
- **V6 (v2)** — Charge : 10 rooms simultanées × 5 joueurs sur le VPS sans saturation (preuve : log ressources).
## Rapport (format obligatoire)
```
| ID | Vérification | Méthode / Preuve | PASS/FAIL | Correctif appliqué |
```
## Interdits
- Aucun compte utilisateur, aucun leaderboard global, aucun stockage de pseudos au-delà de la room.
- Aucune dépendance Google/Firebase/Supabase pour le temps réel.
- Aucune copie de code Geoguess/Geoguess (Vue.js) : référence de design uniquement.
- La v2 ne doit pas régresser le hors-scope du prompt v2 pour le solo.
---
# ADR-002 — Carte SVG/topojson légère pour les écrans hors-jeu (perf 4G)
- **Statut** : Accepté
- **Date** : 2026-07-17
- **Références** : composant carte SVG/topojson d'OpenGuessr Education (inspiration uniquement — licence MIT + Commons Clause = non-commercial, **aucune copie de code**) ; geo.api.gouv.fr (contours de communes GeoJSON, gratuit, sans clé — testé OK) ; prompt v2 §4.1 (lazy-load MapLibre) et §4.5 (perf).
## Contexte
MapLibre GL JS pèse ~800 kB de JS avant la première tile. L'accueil, le récapitulatif final et les fiches « Aprann » n'ont pas besoin d'une carte interactive complète. Sur 4G caribéen, charger MapLibre sur ces écrans détruit le budget < 3 s du prompt v2.
## Décision
- Les écrans **hors-jeu** (accueil 4 régions, mini-carte du récapitulatif, vignette de fiche lieu) utilisent une **carte SVG statique** générée au build depuis un topojson des communes des 4 régions, projection précalculée, aucune tile, aucun JS carte.
- MapLibre reste réservé aux écrans **de jeu** (devinette, résultat, explorer interactif) et reste lazy-loadé.
- Pipeline données : contours communes via `geo.api.gouv.fr/communes?codeRegion=01|02|03|04&fields=nom,code,contour&format=json&geometry=contour` → simplification (mapshaper, seuil à calibrer) → topojson versionné dans le repo → composant Svelte SVG from scratch (MIT).
## Vérifications
- **V1 (bloquant)** — Budget poids : topojson simplifié des 4 régions ≤ 150 kB gzippé ; preuve : taille du fichier + `gzip -9` consignés. Si dépassement : simplifier davantage ou découper par région avec import dynamique.
- **V2 (bloquant)** — LCP accueil < 2,5 s en throttling 4G (Lighthouse mobile, 3 runs, médiane consignée).
- **V3** — Rendu sans JS : la carte SVG s'affiche en SSG avec JS désactivé ; chaque région est un `<a>` ou `<button>` réel (accessibilité clavier, lecteur d'écran : nom de région en FR + créole).
- **V4** — Attribution données : © contributeurs OpenStreetMap / réutilisation données publiques mentionnée dans le footer (licence ODbL pour les contours dérivés OSM le cas échéant).
- **V5** — Zéro code copié d'OpenGuessr Education : revue de provenance, le composant est écrit from scratch (licence MIT du projet).
## Rapport (format obligatoire)
```
| ID | Vérification | Méthode / Preuve | PASS/FAIL | Correctif appliqué |
```
## Interdits
- Ne pas charger MapLibre (ni son CSS) sur l'accueil ou le récapitulatif.
- Ne pas embarquer de tuiles raster de substitution (le SVG est vectoriel pur).
- Ne pas copier le code d'OpenGuessr Education (Commons Clause).
---
# Annexe — Étude Geoguess/Geoguess (référence, 2026-07-17)
- **Projet** : clone GeoGuessr open source, solo + multijoueur par rooms (nom de room partagé, ≤ 5 joueurs), PWA, cartes custom GeoJSON, licence **MIT**.
- **Stack** : Vue.js + Google Maps StreetView + Firebase → **non réutilisable** (doctrine zéro Google ; Vue ≠ Svelte).
- **Signal fort** : leur README admet que la démo publique est limitée par le **prix de l'API Google** et renvoie vers l'auto-déploiement avec clé personnelle — justification directe du zéro propriétaire OKI, à citer dans l'ADR d'architecture initial.
- **À retenir** : (1) le modèle de room sans compte (base UX de l'ADR-001 v2) ; (2) le repo **GeoGuess-Maps**, liste de cartes communautaires — modèle pour les futurs map-packs JWE contribués par la communauté OKI/AKILPA via PR, sans CMS.
-81
View File
@@ -1,81 +0,0 @@
# Mission : JWE — Audit post-exécution, vérifications obligatoires & recommandations
## À exécuter par chaque agent à la fin de sa tâche, avant de la marquer « terminée »
La refonte JWE a été produite à partir du prompt de mission v2 (Svelte 5 + SvelteKit 2, MapLibre, SQLite, 4 régions : Guadeloupe, Martinique, Guyane, La Réunion). Cet audit est la seconde passe. **Aucune tâche n'est terminée sans son rapport de vérification** (format §6). Un FAIL non résolu bloque la livraison. Si le correctif dépasse le scope de l'agent, l'agent produit une RECOMMANDATION argumentée au lieu de coder.
---
## 0. Faits établis (à lire avant tout — ils corrigent le prompt v2)
1. **Le QID d'exemple du prompt v2 était faux** : Fort Delgrès = `Q3077840` (vérifié), pas `Q2216838`. Ses coordonnées officielles sont 15°5919″N, 61°4323″O (≈ 15.9886, -61.7231), pas (16.0036, -61.7319). Si le seed ou l'exemple a été copié tel quel, corriger.
2. **Principe absolu : fetch, pas recall.** Tout QID, toute coordonnée, toute URL photo, toute licence doit être vérifié par une requête réelle, jamais depuis la mémoire du modèle.
3. **Les communes de Guyane sont immenses** (Maripasoula ≈ 18 000 km², Saint-Laurent-du-Maroni ≈ 4 800 km²). Un radius de score fixe par difficulté y est injuste.
4. **La Réunion est à ~12 000 km des Antilles** : un `maxBounds` global couvrant les 4 régions rend une carte inutilisable (95 % d'océan), et un zoom automatique sur la région du lieu au début du round révèle la réponse.
5. **Un lieu sans page Wikipedia a rarement une photo sur Wikimedia Commons** : le piège « 2-3 lieux sans article frwiki avec photo libre » exige des photos propres (collectif OKI) ou Panoramax.
---
## 1. Agent Contenu — vérifications données
- **C1 — QID valides (bloquant).** Pour CHAQUE lieu avec `wikidata_id` non null :
```
curl "https://www.wikidata.org/w/api.php?action=wbgetentities&ids=<QID>&props=sitelinks|labels&languages=fr&format=json" -H "User-Agent: JWE-OKI/1.0"
```
Le sitelink `frwiki` doit exister et correspondre au lieu (comparer le label). Preuve : tableau `lieu → QID → sitelink frwiki → OK/KO` dans le rapport.
- **C2 — Les « null » sont de vrais null (bloquant).** Pour chaque lieu à `wikidata_id: null` intentionnel, prouver par une recherche API (`action=query&list=search` sur frwiki) qu'aucun article frwiki n'existe. Sinon le lieu ne teste pas le Cas B.
- **C3 — Coordonnées précises.** Écart < 500 m avec la source officielle (Wikidata `P625`, IGN, geo.api.gouv.fr). Corriger le seed Fort Delgrès (voir §0.1).
- **C4 — Photos sans fuite.** (a) Noms de fichiers opaques (`uuid.webp`, jamais `fort-delgres.webp` — le nom ne doit pas révéler la réponse dans les devtools). (b) EXIF strippés : preuve `exiftool <fichier>` sans GPS ni appareil. (c) Licence vérifiable : page de description Commons atteignable, crédit et licence exacts dans le JSON.
- **C5 — Photos des lieux « null ».** Source documentée : photo propre OKI (crédit collectif) ou Panoramax (instance + ID + licence). Si introuvable : remplacer le lieu, ne jamais garder une photo sans licence claire.
## 2. Agent Architecte — vérifications décisions
- **A1 — Score juste en Guyane (bloquant).** Vérifier que la formule utilise un radius effectif : `radius_eff = max(radius_difficulté, rayon_commune)` avec `rayon_commune ≈ sqrt(aire_commune / π)`. L'aire peut être précalculée dans le JSON du lieu (champ `aire_commune_km2`) ou dérivée du contour geo.api.gouv.fr. Test à produire : un guess à 40 km du chef-lieu de Maripasoula ne doit pas être quasi-nul.
- **A2 — Pas de fuite de région (bloquant).** Au début d'un round, la caméra ne doit révéler ni la région ni la commune du lieu. Si la solution retenue est la devinette en deux temps (choix de la région via 4 boutons → zoom → placement du marqueur), vérifier que le scoring combine les deux étapes et que c'est documenté dans l'ADR. Si une autre solution a été codée, vérifier l'absence de fuite et l'usage mobile.
- **A3 — Chaîne Wikipedia exacte.** `wbgetentities(props=sitelinks)` → titre frwiki → `fr.wikipedia.org/api/rest_v1/page/summary/{titre}`. Cache ≥ 24 h (pas de « temps réel »). User-Agent explicite côté proxy. Fallback de langue documenté : fr → en. **Cas intermédiaire tranché et documenté** : QID existant mais pas d'article frwiki → recommandé : afficher le Cas B variante « pas encore d'article en français — et si tu le traduisais/créais ? ».
- **A4 — Décision Strapi vs SvelteKit.** L'ADR doit appliquer le critère du prompt (contributeurs non-devs ?). En l'absence de contributeurs non-devs aujourd'hui, la cohérence doctrine OKI = SvelteKit seul + JSON versionné + script de validation au commit. Si Strapi a été retenu, l'ADR doit justifier par des contributeurs réels identifiés.
- **A5 — Hors scope respecté.** Pas de comptes utilisateurs, pas de leaderboard serveur, pas de multijoueur. Scores en `localStorage`. Tout écart = FAIL ou RECOMMANDATION de suppression.
## 3. Agent Back-end — vérifications (si applicable)
- **B1 — Endpoint lieu aléatoire sans fuite (bloquant).** Preuve `curl` : la réponse ne contient ni `coordonnees`, ni `commune`, ni indice de région ; URL photo opaque.
- **B2 — Score calculé côté serveur (bloquant).** Le client envoie `{lieu_id, lat, lon, région_choisie?, indices_utilisés, temps}` ; le serveur calcule (formule A1) et renvoie score + distance + coordonnées réelles. Le client ne calcule rien.
- **B3 — Proxy Wikipedia.** Cache effectif (headers ou store, TTL ≥ 24 h), User-Agent conforme, **aucun texte d'article stocké en base** (contrainte §2.3 du prompt v2).
- **B4 — Pas de dépendance propriétaire.** Audit `package.json` : zéro clé API payante, zéro Google/Mapbox, télémétrie désactivée.
## 4. Agent Front-end — vérifications
- **F1 — Carte.** Bounds effectifs, aucune fuite de région (voir A2), MapLibre lazy-loadé (vérifier le chunk réseau : pas de JS MapLibre sur l'accueil), boutons zoom visibles, navigation clavier.
- **F2 — Cas A / Cas B (bloquant).** Les deux écrans de résultat sont **visuellement distincts** (couleur, icône), le message de lacune est présent en FR **et** en créole, et le CTA « Kontribyé sou Wikipédia » ouvre bien l'éditeur/recherche pré-remplie ou le guide OKI. Captures à l'appui.
- **F3 — Cas intermédiaire géré.** QID présent mais pas d'article frwiki : pas d'écran vide ni d'erreur console ; comportement conforme à la décision A3.
- **F4 — Écran résultat enrichi.** Si le contour de commune est intégré (geo.api.gouv.fr, gratuit sans clé : `https://geo.api.gouv.fr/communes?code=<insee>&fields=nom,contour&format=json&geometry=contour`), vérifier l'affichage du polygone et le respect de `prefers-reduced-motion` sur l'animation du marqueur.
- **F5 — Doctrine OKI.** Une seule action primaire par écran ; bouton Valider en zone du pouce sur mobile ; timer non anxiogène ; micro-interactions en créole ; style carte non-Google.
- **F6 — Performance 4G caribéen.** Premier écran < 3 s en throttling 4G ; photos WebP/AVIF avec `srcset` et lazy loading ; poids d'un round (hors tiles) mesuré et indiqué dans le rapport.
## 5. Agent QA — passe finale
- **Q1 — Rejouer les bloquants.** C1, C2, A1, A2, B1, B2, F2 au minimum, avec preuves.
- **Q2 — Partie complète** dans chaque mode (Konnèt moniman, Kote mwen ye ?, Défi 5 rounds, Aprann), mobile et desktop, dont au moins un round tombant sur un lieu Cas B et un lieu guyanais.
- **Q3 — Checklist OKI du prompt v2 §4 (Agent Intégration & QA)** intégralement passée.
- **Q4 — Scope creep.** Signaler toute feature absente du prompt v2 (auth, comptes, partage réseau social, analytics…).
- **Q5 — Core Web Vitals** mobile 4G mesurés (LCP, INP, CLS) et consignés.
---
## 6. Format du rapport de vérification (obligatoire, un par agent)
```
| ID | Vérification | Méthode / Preuve (commande, requête, capture) | PASS/FAIL | Correctif appliqué |
|----|--------------|-----------------------------------------------|-----------|--------------------|
```
Suivi d'une section **Recommandations** pour tout ce qui dépasse le scope, triée : `bloquant` / `important` / `confort`, avec pour chacune : problème, solution proposée, effort estimé (S/M/L), fichier(s) concerné(s).
Commits atomiques préfixés : `verify:` (preuves), `fix:` (correctifs), `docs:` (ADR, rapport).
## 7. Interdits pendant l'audit
- Aucune nouvelle feature (l'audit corrige et recommande, il n'étend pas).
- Aucun refactor hors correctif de FAIL.
- Aucun PASS sans preuve exécutable ou capture.
- Ne pas « réparer » une donnée (QID, coordonnée, licence) depuis la mémoire : requête réelle ou suppression du lieu.