feat(session-0): cadre juridique, porte de validation et dataset vérifié

Session 0 du plan — aucun code applicatif.

Cadre
- 5 ADR : licences, Phaser 4 hors budget initial, frontière Svelte/Phaser,
  filières A/B des assets, kréyòl langue principale (norme gwadloupéyenne).
- LICENSE (AGPL-3.0-or-later, texte canonique gnu.org), LICENSE-CONTENT
  (CC BY-SA 4.0). Le jeu est une œuvre collective : aucun compositing sur un
  média SA ou NC, contrainte vérifiée par script.
- Audit des 7 sources avec endpoints réellement appelés. TRAMIL vérifié en
  direct : « all rights reserved » et page de mentions légales en 404.
  iNaturalist mesuré à 93 % de CC BY-NC sur un échantillon de 305 photos.

Outillage (tools/data/, hors bundle, zéro appel réseau au runtime)
- verify:taxa, ingest:commons, build:dataset, validate:data, test:porte,
  build:credits, check:audio.
- validate.mjs est la porte exigée par le brief §2 : le build échoue si une
  épreuve s'appuie sur un média non validé, non photographique ou de filière B,
  sur une attribution sans auteur, sur un compositing SA/NC, ou sur une entrée
  publiée sans relecture kréyòl. test:porte prouve les 9 refus + 1 témoin.

Données
- 13 entrées (8 fruits + 5 plantes du module Pa touché) vérifiées contre
  Wikidata ET GBIF : 13/13 ACCEPTED, 0 divergence avec la graine.
- 130 candidats Commons sous licence libre, 39 pré-inscrits, 0 validé —
  aucun agent ne peut valider un média.

Écarts signalés, non corrigés en silence
- Le champ danger de la graine a deux formes (note vs resume) : normalisé.
- 3 identifiants divergent du nom kréyòl au-delà des accents : non tranchés.
- comestible:false sur akée et pomme-cajou : sous-estimation prudente assumée.
- Le mancenillier n'a aucune photographie moderne dans ses 3 premiers
  candidats — l'entrée la plus critique est la moins bien servie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
OKI
2026-07-26 23:47:27 -04:00
co-authored by Claude Opus 5
parent f0b3ce8484
commit 945a524b44
45 changed files with 8599 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# ADR 0001 — Licences du code, du contenu et des médias
- **Statut :** accepté
- **Date :** 2026-07-27
- **Session :** 0 — cadre et données
- **Décideur :** le porteur (ORGANISATION KA INTERNATIONALE)
## Contexte
Le pack d'origine parlait de « ressources libres de droit ». L'expression n'a pas
d'existence juridique et masque quatre régimes très différents :
- **Wikimedia Commons** n'héberge que du libre, mais majoritairement du **CC BY-SA**,
qui impose l'attribution nominative **et** le partage à l'identique. Le partage à
l'identique contamine les œuvres dérivées.
- **iNaturalist** publie beaucoup de **CC BY-NC**, incompatible avec toute distribution
commerciale.
- **TRAMIL** (tramil.net) n'accorde aucune licence dans ses mentions légales. Par défaut :
tous droits réservés.
- **CIRAD / INRAE** appliquent des régimes mixtes, majoritairement propriétaires, avec des
usages pédagogiques négociables au cas par cas.
Le projet vise des enfants de 7 à 12 ans dans un cadre scolaire. Une erreur de licence sur
un support diffusé en classe n'est pas rattrapable par un correctif.
## Décision
### Code — AGPL-3.0-or-later
`LICENSE` contient le texte canonique récupéré depuis gnu.org. L'AGPL plutôt que la GPL
parce qu'une version future pourrait exposer une face serveur (export de progression pour
un enseignant, tableau de classe) : l'AGPL couvre l'usage en réseau, la GPL non.
### Contenu rédigé par OKI — CC BY-SA 4.0
Textes, fiches, indices botaniques, module « Pa touché », données structurées. Cohérent
avec Commons, donc réutilisable par le projet lui-même et par les enseignants.
### Médias tiers — licence d'origine conservée, œuvre collective
Chaque média garde sa licence. Le jeu est une **œuvre collective**, pas une œuvre dérivée.
C'est cette qualification qui permet à des médias CC BY-SA et CC BY-NC de coexister dans
un même produit.
### Clause non commerciale — acceptée, et irréversible
Le porteur a tranché en session 0 : le CC BY-NC est accepté pour élargir le fonds
photographique, en échange d'un **engagement non commercial définitif** sur le jeu.
Le code reste réutilisable commercialement sous AGPL une fois séparé des médias.
## Conséquences
### Ce que cette décision interdit, techniquement
1. **Aucun compositing.** Un média sous partage à l'identique ou sous clause non
commerciale ne peut être ni fusionné, ni détouré puis recombiné, ni fondu dans un atlas
de textures partagé avec d'autres médias. C'est ce qui préserve la qualification
d'œuvre collective.
2. **Aucune vente**, aucune version payante, aucune licence à un éditeur tant qu'un seul
média NC est embarqué.
3. **Aucun fichier TRAMIL.** TRAMIL se cite comme source de vérification factuelle, dans
`sources_factuelles`, jamais comme source d'images.
### Ce qui applique la décision
- `npm run validate:data` refuse tout média `composite: true` portant une clause SA ou NC,
et tout média à attribution dont l'auteur est absent.
- `npm run test:porte` prouve que ces refus se produisent réellement.
- `npm run build:credits` régénère `CREDITS.md` depuis les données, avec une section
distincte pour les médias NC. Un fichier de crédits tenu à la main se désynchronise, et
une attribution fausse est une violation.
- Le schéma impose `licence`, `auteur`, `url_fichier` et `url_description` sur chaque média.
### Condition de réexamen
Si le projet devait un jour être distribué commercialement, il faudrait retirer tous les
médias NC et régénérer les crédits. `CREDITS.md` liste ces médias dans une section dédiée
précisément pour rendre cette opération possible. À ce jour, aucun média NC n'est embarqué :
les 39 médias candidats proviennent tous de Commons, sous CC0, domaine public, CC BY ou
CC BY-SA. **L'engagement non commercial n'est donc pas encore consommé** — il ne le sera
qu'au premier média iNaturalist retenu.
## Alternatives écartées
- **Refuser le NC.** Aurait gardé une porte de sortie commerciale, au prix d'un fonds
photographique plus pauvre sur les espèces mal couvertes (zikak, pòm-sitè).
- **Isoler le NC dans un pack séparé retirable par flag.** Meilleur des deux mondes sur le
papier, mais complexifie le pipeline d'assets et le service worker pour une option dont
le porteur a jugé qu'il ne l'exercerait pas.
@@ -0,0 +1,63 @@
# ADR 0002 — Phaser 4 et l'écart au budget JS
- **Statut :** accepté, avec mesure à compléter en session 4
- **Date :** 2026-07-27
- **Session :** 0 — cadre et données
## Contexte
Le playbook OKI §2.7 fixe un budget non négociable de **170 Ko de JS initial compressé**.
Le mode arcade « Ranmasé » demande de la physique, des particules et du *game feel* : le
faire en DOM coûterait plus cher en travail et en performance qu'un moteur éprouvé.
Le pack d'origine prescrivait « Phaser 5 ». **Phaser 5 n'existe pas.** Vérification faite
au démarrage de la session :
```
$ npm view phaser version
4.2.1
```
La branche courante est Phaser 4 — 4.0 « Caladan » (avril 2026), 4.1 « Salusa »,
4.2 « Giedi » — avec un moteur WebGL réécrit en *render nodes*. Partir de Phaser 3 aurait
été partir d'une branche en fin de vie.
## Décision
Utiliser **`phaser@^4`** (4.2.1 au moment de la décision), et assumer un **écart documenté**
au budget JS, borné par quatre conditions :
1. Phaser n'est **jamais** dans le chemin critique. Import dynamique dans la route
`(jeu)/ranmase` uniquement, jamais au layout racine.
2. Le shell reste **≤ 170 Ko gzip**, Phaser exclu. Le budget n'est pas relâché : il est
mesuré à part.
3. L'entrée dans la scène passe derrière un **écran de chargement designé** (playbook P5,
en KA avec le tambour `ka`), pas derrière une page blanche.
4. Rendu `AUTO` (WebGL avec repli canvas), DPR plafonné à **1,5**,
`powerPreference: 'low-power'`, cible **30 fps stables sur Mali-G52**.
## Conséquences
- Le budget d'entrée du site (≤ 2 Mo) et les Core Web Vitals (LCP < 2,5 s, INP < 200 ms,
CLS < 0,05) restent mesurés **sur l'accueil et sur une fiche**, pages qui ne chargent
jamais Phaser.
- Un utilisateur qui ne joue jamais au mode arcade ne télécharge jamais Phaser. Les quatre
autres modes sont en Svelte pur (ADR 0003).
- La session 4 doit renseigner ici la **mesure réelle** du bundle Phaser en gzip et le
relevé de fps sur mobile d'entrée de gamme.
### Mesures — à compléter en session 4
| Mesure | Cible | Relevé |
|---|---|---|
| Shell JS initial (gzip) | ≤ 170 Ko | _à mesurer_ |
| Bundle Phaser seul (gzip) | information | _à mesurer_ |
| fps scène Ranmasé sur Mali-G52 | ≥ 30 stables | _à mesurer_ |
| Poids total de l'accueil | ≤ 2 Mo | _à mesurer_ |
## Condition de réexamen
Si la mesure de session 4 montre que Phaser dépasse ~450 Ko gzip, ou que les 30 fps ne
sont pas tenus sur mobile d'entrée de gamme, la scène « Ranmasé » est réécrite en DOM/CSS
avec une boucle rAF maison — le playbook P3 (« DOM d'abord, WebGL seulement où ça paie »)
reprend alors la main. Les quatre autres modes n'en dépendent pas : le jeu reste entier.
+59
View File
@@ -0,0 +1,59 @@
# ADR 0003 — Frontière stricte entre Svelte et Phaser
- **Statut :** accepté
- **Date :** 2026-07-27
- **Session :** 0 — cadre et données
## Contexte
Le pack d'origine supposait que le jeu entier serait porté par le moteur de jeu. Or trois
des cinq modes — Rekonèt, Kaché, Sézon — sont des interfaces de cartes et de boutons. Un
canvas les rendrait moins accessibles, plus lourds et plus difficiles à rendre lisibles
sans JavaScript, pour aucun gain.
Le public visé lit parfois mal, navigue sur Android d'entrée de gamme, et une partie de
l'usage se fera en classe, potentiellement au clavier ou avec un lecteur d'écran.
## Décision
Architecture **hybride à frontière stricte** : Svelte porte la coquille, Phaser porte un
seul îlot.
```
Svelte 5 (coquille) Phaser 4 (îlot)
├─ routes, i18n, SEO └─ scène « Ranmasé » uniquement
├─ menus, HUD, album · import dynamique dans (jeu)/ranmase
├─ fiches encyclopédiques · jamais au layout racine
├─ données, progression · game.destroy(true) au démontage
├─ audio, réglages · communication par événements
└─ Rekonèt · Kaché · Sézon · aucun état Svelte partagé par frame
```
Règles de frontière, non négociables :
1. **Import dynamique uniquement**, dans la route de la scène. Un `import` statique de
Phaser au layout racine ferait entrer 300 à 400 Ko dans le chemin critique de toutes
les pages, y compris de l'encyclopédie.
2. **Destruction symétrique** : `game.destroy(true)` au démontage du composant. Un moteur
de jeu qui survit à sa route fuit de la mémoire et continue de consommer du rAF.
3. **Communication par événements**, jamais par état Svelte lu ou écrit à chaque frame.
Le playbook §3 l'interdit explicitement : les états réactifs par frame passent par des
variables plain et un flush rAF.
4. **Miroir DOM sémantique obligatoire** pour la scène (playbook §2.8, point 4) : le canvas
est `aria-hidden`, et l'état du jeu est doublé en DOM lisible au clavier et au lecteur
d'écran.
## Conséquences
- Les quatre modes non-arcade sont **jouables au clavier** et dégradent proprement.
- La session 2 doit produire un jeu **déjà amusant sans Phaser**. C'est la porte de sortie
de cette session : si le jeu n'est pas plaisant en Svelte pur, ajouter un moteur de jeu
ne le sauvera pas — cela ajoutera du poids à un problème de game design.
- L'encyclopédie `/zerbaj/` ne dépend d'aucune brique de jeu et reste prérendue, lisible
sans JavaScript.
## Alternative écartée
**Tout en Phaser.** Aurait unifié le rendu au prix de l'accessibilité clavier, du
référencement de l'encyclopédie, de la lisibilité sans JS et du budget. Le playbook P3 est
explicite : DOM d'abord, WebGL seulement où ça paie. Ici, il ne paie que sur une scène.
+83
View File
@@ -0,0 +1,83 @@
# ADR 0004 — Deux filières d'assets : vérité et décor
- **Statut :** accepté
- **Date :** 2026-07-27
- **Session :** 0 — cadre et données
## Contexte
Le pack d'origine prévoyait de générer l'intégralité des visuels du jeu — 76 images — par
diffusion (SDXL / FLUX via ComfyUI), puis de passer directement à la production.
C'est une contradiction avec le but du jeu. Un enfant qui apprend à reconnaître un
corossol sur une image générée n'apprend pas à reconnaître un corossol : il apprend à
reconnaître ce qu'un modèle de diffusion produit quand on lui demande un corossol. Sur les
espèces peu représentées dans les jeux d'entraînement — le zikak, la quenette, le cachiman
cœur-de-bœuf, la pomme-liane — le modèle ne refuse pas : il produit un fruit **plausible
et faux**. C'est le pire des deux mondes, parce que l'erreur est invisible à qui ne connaît
pas déjà l'espèce.
Cette session a produit une illustration involontaire du même problème côté sources
réelles : parmi les photographies que Wikimedia Commons remonte pour *Melicoccus bijugatus*
(la quenette), l'un des premiers résultats est une photographie de **singe capucin**
mangeant le fruit. Aucune machine — moteur de recherche ou modèle génératif — ne peut
tenir lieu de validation botanique.
## Décision
Deux filières, deux régimes, jamais mélangés.
| Filière | Contenu | Source autorisée | Rôle |
|---|---|---|---|
| **A — Vérité** | fruit, coupe, feuille, écorce, port de l'arbre | photographie réelle : Commons, iNaturalist, herbier, appareil du porteur | tout ce qui sert à **identifier** — épreuves, indices, fiches |
| **B — Décor** | fonds, personnages, UI, sprites arcade, badges, écrans de titre | génération IA (ComfyUI / FLUX) | tout ce qui **ne sert pas** à identifier |
### Une troisième catégorie découverte en session 0
Les fonds Commons contiennent beaucoup de **planches botaniques anciennes** — Blanco,
Merian, Köhler, domaine public, exactes et magnifiques. Elles ne sont ni des photographies
ni des images générées. Le schéma les distingue donc explicitement :
```
type_media : "photo" | "planche_botanique" | "image_generee"
```
Une planche botanique **documente** une fiche encyclopédique. Elle ne peut **pas** porter
une épreuve d'identification : une gravure au trait rehaussée n'entraîne pas l'œil à
reconnaître un fruit posé sur une table de marché.
### Le champ `usages` et la porte
Chaque média déclare où il est employé : `epreuve`, `indice`, `fiche`, `decor`. Les deux
premiers sont les usages **identifiants**. Un média identifiant doit satisfaire les quatre
conditions, vérifiées par script :
1. `filiere: "A"` ;
2. `type_media: "photo"` ;
3. `valide: true`, avec un `valide_par` nommé — une validation anonyme n'engage personne ;
4. `vue` renseignée.
## Conséquences
- `npm run validate:data` fait échouer le build sur toute violation. `npm run test:porte`
rejoue les neuf cas interdits et vérifie que le refus se produit — une règle qu'on n'a
jamais vue refuser quelque chose est une intention, pas une règle.
- **Tout média sort de l'ingestion avec `valide: false`.** Aucun agent, aucun script, aucun
modèle ne peut passer ce champ à `true`. La validation se fait par comparaison à au moins
deux photographies de référence, idéalement contresignée par quelqu'un qui connaît :
TRAMIL, un enseignant de SVT, un agriculteur, une grand-mère.
- Le pipeline de génération (filière B) vit dans `tools/assets/`, hors du bundle, avec son
propre `requirements.txt`. Il est piloté **hors ligne, au build** (playbook P2). Aucun
appel réseau ne subsiste au runtime du jeu.
- Les défauts du script du pack d'origine sont corrigés en session 3 :
`hash()` de Python est randomisé par exécution (`PYTHONHASHSEED`), donc les seeds annoncés
comme reproductibles ne le sont pas — utiliser `zlib.crc32(clé.encode()) & 0xFFFFFFFF` ;
`copy.deepcopy` au lieu de `json.loads(json.dumps(...))` ; et vérifier la licence des
**poids** du modèle `rembg`, qui ne suit pas nécessairement la licence MIT du paquet.
## Alternative écartée
**Générer les visuels d'identification et les faire valider a posteriori.** Le pack le
proposait implicitement. Le problème n'est pas la validation, c'est le coût de l'erreur non
détectée : un validateur non spécialiste confirmera un corossol plausible. La photographie
supprime la question.
+78
View File
@@ -0,0 +1,78 @@
# ADR 0005 — Le kréyòl est la langue principale, norme gwadloupéyenne
- **Statut :** accepté ; orthographe en attente de relecture humaine
- **Date :** 2026-07-27
- **Session :** 0 — cadre et données
## Contexte
Le jeu vise à restituer une connaissance qui se transmettait en kréyòl. Traiter le kréyòl
comme une traduction du français inverserait le rapport que le projet cherche précisément
à rétablir.
Par ailleurs, les orthographes gwadloupéyenne et matinitjée diffèrent sur plusieurs entrées
du jeu de données. Les mélanger produirait un contenu que personne ne reconnaît tout à
fait comme le sien.
## Décision
### Le nom kréyòl est le nom de l'entrée
Dans les données, `noms.gcf` est le **nom principal**. `noms.fr`, `noms.en` et `noms.es` en
sont les traductions. Le schéma rend `gcf` obligatoire et les autres facultatives — pas
l'inverse. C'est un choix politique, pas cosmétique, et il ne s'inverse pas au motif que le
français serait « plus pratique » pour l'interface.
### Routes
`/` sert le kréyòl, `/fr/` le français, `/en/` l'anglais. Routeur par dossiers avec
catalogues JSON, pas de middleware (playbook §5b.7) : les URLs et le prérendu restent sous
contrôle total, ce qui est la condition du statique et du hors-ligne.
Attribut `lang="gcf"` correct partout, y compris sur les fragments kréyòl insérés dans une
page française — un nom de fruit prononcé par un lecteur d'écran avec la phonétique
française n'est pas le nom du fruit.
### Norme orthographique : gwadloupéyenne
Une seule norme pour tout le projet, décidée par le porteur en session 0. Le champ
`orthographe.norme` vaut `"gwadloupeyen"` sur chaque entrée, et le schéma n'accepte pas
d'autre valeur : il est impossible de mélanger les deux normes sans modifier le schéma,
donc sans que la décision soit revue.
### L'orthographe elle-même n'est pas du ressort de l'agent
Aucune graphie n'est tranchée par un agent de code. Chaque entrée porte :
```json
"orthographe": { "norme": "gwadloupeyen", "statut": "a_relire", "relu_par": null }
```
`statut` ne passe à `"relu"` que par décision d'un locuteur humain nommé. Le validateur
refuse toute entrée déclarée publiable dont l'orthographe n'est pas relue, et toute entrée
`relu` sans nom de relecteur.
Les incertitudes constatées sont consignées dans `docs/kreyol-a-relire.md` — signalées, pas
corrigées.
## Conséquences
- Les 13 graphies issues de `fruits.seed.json` sont **toutes non vérifiées** à ce stade. La
graine elle-même le déclare. Trois incohérences internes ont été relevées entre
l'identifiant technique et le nom kréyòl (voir `docs/kreyol-a-relire.md`) : elles ne sont
pas corrigées ici.
- Les identifiants techniques (`id`) restent en ASCII sans diacritique — ils servent de nom
de fichier et de segment d'URL. Ils ne sont **pas** une graphie et ne doivent jamais être
affichés à l'utilisateur. `kowosol` est un identifiant ; `kowosòl` est un nom.
- Un identifiant ne se renomme jamais après publication : il est dans les URLs, dans la
progression stockée en local et dans les liens partagés. Une correction d'orthographe
change `noms.gcf`, jamais `id`.
- Chaque entrée doit recevoir un enregistrement audio du nom prononcé par une voix humaine
locale. Pas de synthèse vocale : les enfants visés lisent mal le kréyòl écrit, dont
l'orthographe leur est peu familière — un jeu qui enseigne les noms kréyòl sans les faire
entendre rate sa cible. `npm run check:audio` tient la liste de ce qui manque.
## Point ouvert
Le relecteur n'est pas encore désigné. Tant qu'il ne l'est pas, aucune entrée ne peut être
publiée : c'est le validateur qui l'impose, pas une note de bas de page.