Files
JWE/docs/audit-oki.md
T

13 KiB
Raw Blame History

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: truescore: 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 ? … : nullbascule 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/triche404 (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-a vert / .cas-b sable-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/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.