# Audit OKI — JWE v2 (Agent Intégration & QA) Date : 2026-07-17 · Périmètre : `app/` (SvelteKit 2 + Svelte 5 + MapLibre GL 5) Méthode : tests **réellement exécutés** sur le build de production (`node build`, PORT=4173) + revue de code. Chaque point est accompagné de sa preuve (commande, extrait). Légende : ✅ conforme · ⚠️ conforme avec réserve / point d'amélioration · ❌ non conforme --- ## 1. Build et vérifications statiques | Test | Résultat | Preuve | |---|---|---| | `npm run check` | ✅ | `svelte-check found 0 errors and 0 warnings` | | `npm run build` | ✅ | `✓ built in 5.05s` + `adapter-node ✔ done` | ### Isolation de MapLibre (bundle initial) - Le chunk MapLibre `xnoNZPIh.js` (**1031 KB brut / 277,8 KB gzip**) n'est chargé que par `import("./xnoNZPIh.js")` **dynamique** depuis le chunk de `GameMap` — vérifié par `grep` dans `build/client/_app/immutable/`. - Le HTML SSR de `/` (5,6 KB) ne contient **0 occurrence** de « maplibre » et ne bootstrap que `entry/app.js` + `entry/start.js`. - Le CSS MapLibre (`maplibre-gl.*.css`, 68,3 KB / 9,8 KB gz) est un asset séparé, chargé par le même import dynamique (`GameMap.svelte:279`). **✅ Le bundle initial ne contient pas MapLibre.** --- ## 2. API — tests fonctionnels (serveur `node build`, PORT=4173) ### 2.1 Anti-triche `/api/round` ✅ 15 tirages variés (sans filtre, `region`, `region+categorie`, `categorie`, `region+categorie+exclude` avec 2 ids exclus) : ``` 15/15 tirages : AUCUNE fuite (clés coordonnees/commune/region/wikidata_id absentes) ``` Payload conforme à `docs/architecture.md` §5 : `{ id, nom, nom_creole, categorie, difficulte, photo{url,credit,alt}, indices[] }`. `Cache-Control: no-store` présent. Les filtres sont respectés (ex. `region=REUNION&categorie=LIEU` → Piton de la Fournaise). *Note : le `nom` du lieu est dans le payload — c'est le choix de la spec (§5), pas une fuite : le jeu nomme le monument à reconnaître.* ### 2.2 Partie complète + cohérence du score ✅ Scénario réel : `GET /api/round?region=GUADELOUPE` → Jardin botanique de Deshaies (DIFFICILE, r=5 km) → `POST /api/guess` avec coords fausses : - Réponse : `distanceKm: 38.5, score: 2` — recalcul indépendant haversine + `round(5000·e^(−38.5/5))` = **2** → écart 0. - Second test (Fort Delgrès, MOYEN, guess à 10,0 km, `hintsUsed: 1`, `timeMs: 30000`) : serveur **2540**, recalcul `min(5000, 2572×0,9 + 2572×0,9×0,2×0,5)` = **2540** → écart 0. Pénalité −10 %/indice et bonus temps ≤ +20 % conformes. - `skip: true` → `score: 0, distanceKm: null`, vérité révélée (extension documentée, cohérente avec le bouton « Passer »). - Erreurs : id inconnu → **404**, coords invalides → **400**, id manquant → **400**, JSON invalide → **400**. ### 2.3 Cas A — lieu avec `wikidata_id` ✅ | Lieu | `/api/guess` | `/api/wiki/{qid}` | |---|---|---| | Fort Delgrès | `wikidata_id: "Q3077840"` | 200, `title: "Fort Delgrès"`, extrait FR **422 car.**, `url: https://fr.wikipedia.org/wiki/Fort_Delgr%C3%A8s` | | Centre spatial guyanais | `wikidata_id: "Q308987"` | 200, extrait FR **447 car.**, url fr.wikipedia | Cache : `cache-control: public, max-age=3600` vérifié par `curl -I` ; cache mémoire TTL 24 h + User-Agent Wikimédia explicite dans `routes/api/wiki/[qid]/+server.ts`. ### 2.4 Cas B — lacune documentaire (CRUCIAL) ✅ Corpus : **9 lieux `wikidata_id: null`** (2 GP, 2 MQ, 3 GUY, 2 REU) — intentionnels, conformes au piège de la spec §Agent Contenu. - `POST /api/guess` sur **Plage de Bois Jolan** (`ff00fd1d…`) et **Anse Noire** (`cd652fb7…`) → `wikidata_id: null` dans les deux réponses. ✅ - Rendu (`WikiFiche.svelte:64-78`) : `wikidataId` null → panneau **`.cas-b`** visuellement distinct (fond `--oki-lacune-fond` #fdf3e3, bordure 2px `--oki-lacune-bord` #d9a441, icône ◌ en pointillés) vs `.cas-a` vert doux. ✅ - Titre exact de la spec : **« Pwen blindé — Sa pa dokimanté »** (`fr.ts:68`, `gcf.ts:70`). ✅ - Texte FR conforme à §4.3 (« Ce lieu existe, mais il n'a pas de page sur Wikipédia ni de fiche sur Wikidata… qui alimentent même l'intelligence artificielle). ») + équivalent créole complet dans `gcf.ts:71-72`. ✅ - CTA **« Kontribyé sou Wikipédia »** → `https://fr.wikipedia.org/w/index.php?search={nom du lieu}&title=Spécial:Recherche` — titre du lieu pré-rempli dans la recherche (`WikiFiche.svelte:38-40`) + lien secondaire vers le guide « Premiers pas ». ✅ - **Chemin « qid non null mais wiki 404 »** : `/api/wiki/Q15646372` (entité sans sitelink fr) → **404** ; dans `WikiFiche.svelte:28`, `res.ok ? … : null` → **bascule bien sur le panneau Cas B** (même comportement sur erreur réseau, ligne 29-30). ✅ - Chaîne « Pwen blindé » présente dans le bundle client livré (`grep` dans `build/`). ✅ ### 2.5 `/api/lieux` ✅ 40 lieux ; toutes les coords sont des **multiples exacts de 0,05** (vérifié par script, ex. Fort Delgrès → `{lat: 16, lon: -61.7}`). `Cache-Control: public, max-age=300`. ### 2.6 Pages ✅ `/` 200 · `/jeu/monument` 200 · `/jeu/lieu` 200 · `/jeu/defi` 200 · `/explorer` 200 · `/manifest.webmanifest` 200 · `/service-worker.js` 200 · `/icon.svg` 200 · `/jeu/triche` → **404** (mode invalide rejeté). SSR vérifié : le HTML de `/` contient `

JWE

` rendu serveur (contenu visible avant hydratation). --- ## 3. Parité i18n FR / créole ✅ Script Node comparant les clés aplaties de `fr.ts` et `gcf.ts` : ``` clés fr : 77 | clés gcf : 77 manquantes dans gcf : aucune — manquantes dans fr : aucune ``` Le message de lacune existe dans les DEUX langues (titre, texte, CTA, guide). Double garantie : `gcf` est typé `Dict` (`gcf.ts:3`), donc toute clé manquante casserait `npm run check`. L'attribut `` est mis à jour au changement de langue (`store.svelte.ts:25`). --- ## 4. A11y / doctrine OKI (revue de code) | Point | Verdict | Preuve | |---|---|---| | `prefers-reduced-motion` | ✅ | Règle globale `oki.css:111-117` ; vol du marqueur désactivé (`GameMap.svelte:56-57,116-117`) et transitions cartes à `duration: 0` ; transition du bottom-sheet coupée (`explorer/+page.svelte:256-260`) | | Navigation clavier carte | ✅ | Zone carte `role="application"`, `tabindex="0"`, `aria-label` i18n ; flèches = déplacer le marqueur (pas constant en pixels), Entrée = valider (`GameMap.svelte:246-273`) ; focus-visible visible (`oki.css:67-72`) | | Une seule action primaire / écran | ⚠️ | Accueil : « Jouer » seul corail. Jeu : « Valider » seul corail (Passer = secondaire, Indice = tertiaire). Bilan : « Partager » seul. **Réserve** : écran résultat Cas B = bouton corail « Lieu suivant » **+** CTA orange « Kontribyé sou Wikipédia » — deux éléments fortement colorés. Cohérent avec Peak-End (§5), mais en tension avec « une seule action primaire ». À trancher en design review. | | Bouton Valider en zone pouce | ✅ | `.barre-actions` sticky en bas + `env(safe-area-inset-bottom)` (`jeu/[mode]/+page.svelte:374-384`) ; accueil : `.zone-pouce` fixe bas | | Timer non anxiogène | ✅ | `TimerRing` : cercle qui se vide, couleur douce `--oki-vert-clair`, **aucun chiffre**, **aucune échéance** (à 0 le tour continue, le bonus s'arrête juste) ; timer désactivé par défaut | | Pas de mécanique dopaminergique | ✅ | `grep -i 'streak|leaderboard|classement|loot|confetti'` → 0 résultat ; pas de session serveur, pas de compte, « Passer » = 0 point sans pénalité, ton encourageant (« Kité sa ye ? ») | | Zoom contraint aux 4 régions | ✅ | `maxBounds` [-63,-23,57,18] + `minZoom: 4` (`regions.ts`) + 4 boutons « saut de région » avec `aria-label` groupe | | Photos | ✅ | `loading="lazy" decoding="async"` partout (`PhotoPanel`, `explorer`, `WikiFiche`) ; `alt` descriptif du corpus ; crédit affiché sous chaque photo | --- ## 5. Performance 4G (mesures build + estimation) Pas de Lighthouse disponible dans l'environnement → mesures réelles des artefacts + estimation argumentée. ### Poids mesuré (sur fil, gzip — les `.gz` sont pré-générés dans le build) | Écran | Détail | Poids | |---|---|---| | **Premier écran `/`** | HTML 5,6 KB + JS initial 38,7 KB gz + CSS 1,8 KB gz | **≈ 43 KB** | | `/jeu/*` (supplément) | code page + GameMap | +12,9 KB gz | | MapLibre (lazy, uniquement écrans carte) | JS 277,8 KB gz + CSS 9,8 KB gz | 287,6 KB gz | | Photos | Wikimedia `?width=1200`, lazy | ~150-400 KB/photo | ### Estimation 4G caribéen (≈ 10 Mbps, RTT 150 ms ; et slow 4G 1,6 Mbps) - **Premier écran < 3 s : ✅ large.** ~43 KB → < 0,5 s de transfert même en slow 4G ; SSR → `

` et grille des régions visibles avant hydratation ; police système (aucune webfont) ; zéro requête tierce. - Écran de jeu : +300 KB gz (MapLibre) ≈ 1,5-2 s en slow 4G, chargé **après** l'affichage de la photo (lazy) — le joueur peut déjà observer la photo. - PWA : app shell pré-caché par le service worker → 2e visite quasi instantanée, photos en stale-while-revalidate. --- ## 6. Checklist d'audit OKI (spec §Agent Intégration & QA) - [x] **Une seule action primaire par écran** — ✅ (⚠️ réserve Cas B, voir §4) - [x] **Mobile : bouton Valider en zone du pouce, carte utilisable au doigt** — ✅ (sticky bottom, marqueur draggable, boutons zoom +/- visibles) - [x] **Performance : premier écran < 3 s, MapLibre lazy-loadé** — ✅ (~43 KB gz initial, chunk MapLibre dynamique) - [x] **Éthique : pas de mécanique dopaminergique, score non punitif** — ✅ - [x] **Éducation : les deux cas (Wiki OK / Wiki manquant) fonctionnent et sont distincts visuellement** — ✅ (tests §2.3/§2.4, panneaux `.cas-a` vert / `.cas-b` sable-orangé) - [x] **Bilinguisme FR/créole présent (y compris message de lacune)** — ✅ (77/77 clés) - [x] **PWA installable** — ✅ avec réserve : manifest + SW + icônes SVG `any`/`maskable` valides, **mais aucun PNG 192/512** — Chrome desktop peut refuser l'invite d'installation sans PNG (⚠️). Intégration MapLibre + API Lieux + API Wikipedia : ✅. Partie complète jouée : ✅ (§2.2). --- ## 7. Bugs corrigés pendant l'audit 1. **Libellés des boutons zoom MapLibre en anglais** — Les clés i18n `carte.zoomIn`/`zoomOut` existaient mais n'étaient jamais utilisées : le `NavigationControl` natif affichait les tooltips/aria-labels anglais « Zoom in / Zoom out » sur une UI FR/créole (a11y lecteur d'écran). - **Correctif** : `app/src/lib/components/GameMap.svelte` — option `locale` ajoutée au constructeur `new mod.Map({…})` avec `'NavigationControl.ZoomIn': t().carte.zoomIn` et `'NavigationControl.ZoomOut': t().carte.zoomOut`. - **Vérifié** : `npm run check` 0 erreur, rebuild OK, chaîne présente dans le chunk GameMap reconstruit, serveur relancé et endpoints re-testés (200). Aucun bug structurel trouvé. --- ## 8. Points d'amélioration restants (non bloquants) 1. ⚠️ **Icônes PWA** : ajouter des PNG 192×192 et 512×512 (certains navigateurs n'acceptent pas le SVG seul pour l'invite d'installation). 2. ⚠️ **Contraste du corail** `--oki-corail` #e4572e sur texte blanc ≈ **3,7:1** : conforme AA pour texte large et composants d'interface, juste sous les 4,5:1 du texte courant. Envisager `--oki-corail-fonce` au repos si on veut du AA strict. 3. ⚠️ **Bonus temps basé sur `timeMs` fourni par le client** (`/api/guess`) : un client modifié peut s'accorder +20 %. Sans leaderboard ni enjeu, c'est acceptable — à documenter si un mode compétitif arrive un jour. 4. ⚠️ **Deux accents forts sur l'écran résultat Cas B** (corail « Lieu suivant » + orange « Kontribyé ») : Peak-End assumé, mais à valider contre la règle « une seule action primaire ». 5. **Clés i18n inutilisées** : `resultat.bravo`, `resultat.score`, `micro.pwenBlinde`, `defi.manche` — cosmétique, à nettoyer ou brancher. 6. **Pas de tests automatisés** dans le dépôt (ni unitaires ni e2e) — les vérifications de cet audit sont manuelles ; un `vitest` sur `scoreGuess`/`sanitizeRound` + un test e2e du Cas B sécuriseraient les régressions. 7. **Limite connue** (déjà documentée dans `architecture.md`) : l'enveloppe `maxBounds` rectangulaire inclut l'Atlantique tropical et le Brésil — inoffensif. --- ## Verdict **✅ Conforme à la spec.** Build propre, anti-triche étanche sur 15 tirages, scoring recalculé serveur et vérifié au point près, Cas A et Cas B fonctionnels (y compris le fallback 404 → Cas B), i18n FR/créole complet, doctrine OKI respectée (calm tech, zone pouce, reduced-motion, zéro dark pattern). 1 bug mineur corrigé (locale des contrôles zoom). 7 points d'amélioration non bloquants listés ci-dessus. --- ## Addendum post-audit (vérification finale orchestrateur) - **Fix anti-spoiler** : le crédit photo (`photo.credit`, souvent explicite sur le lieu) était affiché pendant la phase de devinette. Désormais masqué pendant la devinette (`PhotoPanel`, prop `revealed`) et affiché après validation dans `ResultPanel` — attribution CC conservée, indice supprimé. Re-vérifié : `npm run check` 0 erreur, `npm run build` OK.