Files
JWE/docs/audit-oki.md
T

177 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.
# 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 `<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|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 → `<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)
- [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.