13 KiB
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 parimport("./xnoNZPIh.js")dynamique depuis le chunk deGameMap— vérifié pargrepdansbuild/client/_app/immutable/. - Le HTML SSR de
/(5,6 KB) ne contient 0 occurrence de « maplibre » et ne bootstrap queentry/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, recalculmin(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/guesssur Plage de Bois Jolan (ff00fd1d…) et Anse Noire (cd652fb7…) →wikidata_id: nulldans les deux réponses. ✅- Rendu (
WikiFiche.svelte:64-78) :wikidataIdnull → panneau.cas-bvisuellement distinct (fond--oki-lacune-fond#fdf3e3, bordure 2px--oki-lacune-bord#d9a441, icône ◌ en pointillés) vs.cas-avert 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 ; dansWikiFiche.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é (
grepdansbuild/). ✅
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 <h1>JWE</h1> 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 <html lang> 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 |
| 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 →
<h1>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)
- Une seule action primaire par écran — ✅ (⚠️ réserve Cas B, voir §4)
- Mobile : bouton Valider en zone du pouce, carte utilisable au doigt — ✅ (sticky bottom, marqueur draggable, boutons zoom +/- visibles)
- Performance : premier écran < 3 s, MapLibre lazy-loadé — ✅ (~43 KB gz initial, chunk MapLibre dynamique)
- Éthique : pas de mécanique dopaminergique, score non punitif — ✅
- Éducation : les deux cas (Wiki OK / Wiki manquant) fonctionnent et sont distincts visuellement — ✅ (tests §2.3/§2.4, panneaux
.cas-avert /.cas-bsable-orangé) - Bilinguisme FR/créole présent (y compris message de lacune) — ✅ (77/77 clés)
- PWA installable — ✅ avec réserve : manifest + SW + icônes SVG
any/maskablevalides, 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
- Libellés des boutons zoom MapLibre en anglais — Les clés i18n
carte.zoomIn/zoomOutexistaient mais n'étaient jamais utilisées : leNavigationControlnatif 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— optionlocaleajoutée au constructeurnew mod.Map({…})avec'NavigationControl.ZoomIn': t().carte.zoomInet'NavigationControl.ZoomOut': t().carte.zoomOut. - Vérifié :
npm run check0 erreur, rebuild OK, chaîne présente dans le chunk GameMap reconstruit, serveur relancé et endpoints re-testés (200).
- Correctif :
Aucun bug structurel trouvé.
8. Points d'amélioration restants (non bloquants)
- ⚠️ Icônes PWA : ajouter des PNG 192×192 et 512×512 (certains navigateurs n'acceptent pas le SVG seul pour l'invite d'installation).
- ⚠️ 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-fonceau repos si on veut du AA strict. - ⚠️ Bonus temps basé sur
timeMsfourni 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. - ⚠️ 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 ».
- Clés i18n inutilisées :
resultat.bravo,resultat.score,micro.pwenBlinde,defi.manche— cosmétique, à nettoyer ou brancher. - Pas de tests automatisés dans le dépôt (ni unitaires ni e2e) — les vérifications de cet audit sont manuelles ; un
vitestsurscoreGuess/sanitizeRound+ un test e2e du Cas B sécuriseraient les régressions. - Limite connue (déjà documentée dans
architecture.md) : l'enveloppemaxBoundsrectangulaire 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, proprevealed) et affiché après validation dansResultPanel— attribution CC conservée, indice supprimé. Re-vérifié :npm run check0 erreur,npm run buildOK.