2026-07-26 23:47:27 -04:00
#!/usr/bin/env node
/**
* build-dataset.mjs — compose le dataset à partir de la graine et des sorties vérifiées.
*
* fruits.seed.json (contenu éditorial d'amorçage)
* + tools/data/out/taxa-verified.json (Wikidata + GBIF, réponses d'API)
* + tools/data/out/commons-candidats.json (médias sous licence libre)
* → src/lib/data/fruits/<id>.json
*
* AMORÇAGE SEUL. Une fois les fichiers écrits, ils sont édités à la main par le porteur
* (choix des photos, assignation des vues, passage de `valide` à true, corrections kréyòl).
* Le script refuse donc d'écraser un fichier existant sans `--force`, pour ne jamais
* détruire une validation humaine.
*/
import { readFile , writeFile , mkdir , access } from 'node:fs/promises' ;
import { fileURLToPath } from 'node:url' ;
import { dirname , join } from 'node:path' ;
const ROOT = join ( dirname ( fileURLToPath ( import . meta . url )), '..' , '..' );
const DEST = join ( ROOT , 'src' , 'lib' , 'data' , 'fruits' );
const FORCE = process . argv . includes ( '--force' );
/** Nombre de médias candidats pré-inscrits par entrée. Le brief §8 en exige au moins deux. */
const MEDIAS_PAR_ENTREE = 3 ;
/** Licences à attribution — recopié de la classification d'ingest-commons.mjs. */
const SANS_ATTRIBUTION = new Set ([ 'CC0-1.0' , 'PD' ]);
const AVEC_SA = new Set ([ 'CC-BY-SA-2.0' , 'CC-BY-SA-2.5' , 'CC-BY-SA-3.0' , 'CC-BY-SA-4.0' , 'CC-BY-NC-SA-4.0' ]);
const AVEC_NC = new Set ([ 'CC-BY-NC-4.0' , 'CC-BY-NC-SA-4.0' ]);
const slug = ( s ) =>
s
. replace ( /^File:/ , '' )
. replace ( /\.[a-z0-9]+$/i , '' )
. normalize ( 'NFD' )
. replace ( /[\u0300-\u036f]/g , '' )
. toLowerCase ()
. replace ( /[^a-z0-9]+/g , '-' )
. replace ( /^-+|-+$/g , '' )
. slice ( 0 , 60 )
. replace ( /-+$/ , '' );
/**
* Une planche gravée du XVIIIe siècle est exacte mais stylisée : elle ne peut pas servir
* d'épreuve de reconnaissance du réel. On la détecte grossièrement — c'est un pré-tri,
* pas un verdict : le porteur corrige le champ à la relecture.
*/
const INDICES_PLANCHE = /blanco|merian|k[oö]hler|flora|sylva|botanical|plate|lithograph|engrav|descourtilz|1[6-9]\d\d/i ;
function typeMedia ( c ) {
// La date de prise de vue fait partie du texte examiné : sans elle, la planche de
// Jacquin (1780) ressortait en « photo » — son titre et son auteur ne disent rien
// de son siècle. Constaté en session 0, corrigé ici.
const hay = ` ${ c . titre } ${ c . description ?? '' } ${ c . auteur ?? '' } ${ c . date_prise_de_vue ?? '' } ` ;
if ( INDICES_PLANCHE . test ( hay )) return 'planche_botanique' ;
// La photographie de terrain n'existe pas avant le XXe siècle dans ces fonds.
const annee = Number (( c . date_prise_de_vue ?? '' ). match ( /\b(1[5-9]\d\d)\b/ ) ? .[ 1 ]);
if ( annee && annee < 1900 ) return 'planche_botanique' ;
return 'photo' ;
}
/**
* Écart constaté dans la graine : les dangers de niveau « vigilance » (kénèt, pòm-kannèl)
* portent leur texte dans `note`, les niveaux « élevé » et « critique » dans `resume`.
* Le schéma impose un champ unique — on normalise vers `resume` sans rien réécrire.
* Écart signalé dans docs/sessions/00-cadre-et-donnees.md, conformément au brief §9.
*/
function normaliserDanger ( d ) {
if ( ! d ) return null ;
const { note , resume , ... reste } = d ;
return { ... reste , resume : resume ?? note };
}
2026-07-27 10:40:23 -04:00
/**
* Les questions posées aux locuteurs, affichées telles quelles dans le jeu.
*
* Ce sont des QUESTIONS, pas des corrections : le projet n'a pas à trancher l'orthographe
* kréyòl (ADR 0005), mais il a le devoir de dire où il doute plutôt que d'afficher une
* graphie douteuse avec l'aplomb d'une certitude.
*
* Une entrée qui figure ici sort en `statut: "en_discussion"`, ce qui déclenche l'affichage
* de la question dans l'encyclopédie et dans le jeu.
*/
const QUESTIONS_KREYOL = {
ake : "Nous écrivons « aki », mais l'identifiant du fruit dit « ake ». Laquelle des deux graphies est la bonne en Guadeloupe ? Et comment le prononcez-vous chez vous ?" ,
medisinye : "« medsinyé » ou « medisinyé » ? Nous rencontrons les deux. Laquelle employez-vous ?" ,
'lorye-jonn' : "« lorié-jòn » ou « lorye-jonn » ? Nous ne savons pas trancher. Comment l'écrivez-vous ?" ,
'pom-dlo' :
"Selon les îles, « pòm-dlo », « pòm-malaka » et « pòm-rose » ne désignent pas le même fruit. Lequel appelez-vous « pòm-dlo » ?"
};
/**
* Ces questions sont rédigées en français, et c'est un pis-aller assumé : demander en
* français à des locuteurs kréyòl de corriger du kréyòl est un contresens. Mais rédiger
* la question en kréyòl reviendrait à produire du kréyòl non relu au moment même où l'on
* reconnaît ne pas savoir le faire (ADR 0005).
*
* Leur traduction en kréyòl fait donc partie des contributions demandées — c'est la
* première tâche listée dans docs/kreyol-participatif.md.
*/
2026-07-26 23:47:27 -04:00
/** Deux planches Blanco tronquées au même slug : on désambiguïse plutôt que de perdre un média. */
function idsUniques ( medias ) {
const vus = new Map ();
return medias . map (( m ) => {
const n = ( vus . get ( m . id ) ?? 0 ) + 1 ;
vus . set ( m . id , n );
return n === 1 ? m : { ... m , id : ` ${ m . id } - ${ n } ` };
});
}
async function existe ( p ) {
try {
await access ( p );
return true ;
} catch {
return false ;
}
}
async function main () {
const seed = JSON . parse ( await readFile ( join ( ROOT , 'fruits.seed.json' ), 'utf8' ));
const taxa = JSON . parse ( await readFile ( join ( ROOT , 'tools/data/out/taxa-verified.json' ), 'utf8' ));
const medias = JSON . parse ( await readFile ( join ( ROOT , 'tools/data/out/commons-candidats.json' ), 'utf8' ));
const parId = ( arr ) => new Map ( arr . map (( r ) => [ r . id , r ]));
const T = parId ( taxa . resultats );
const M = parId ( medias . resultats );
const dateVerif = taxa . genere_le . slice ( 0 , 10 );
const entrees = [
... seed . fruits . map (( f ) => ({ ... f , categorie : 'fruit' })),
... seed . plantes_dangereuses . map (( p ) => ({ ... p , categorie : 'plante_dangereuse' }))
];
await mkdir ( DEST , { recursive : true });
let ecrits = 0 ;
let ignores = 0 ;
for ( const e of entrees ) {
const chemin = join ( DEST , ` ${ e . id } .json` );
if ( ! FORCE && ( await existe ( chemin ))) {
console . log ( `· ${ e . id } — existe déjà, conservé (--force pour écraser)` );
ignores ++ ;
continue ;
}
const t = T . get ( e . id );
if ( ! t ? . wikidata ? . qid || ! t ? . gbif ? . usageKey ) {
console . error ( `✘ ${ e . id } — taxonomie non vérifiée, entrée non écrite` );
continue ;
}
const candidats = idsUniques (
( M . get ( e . id ) ? . candidats ?? []). slice ( 0 , MEDIAS_PAR_ENTREE ). map (( c ) => ({
id : slug ( c . titre ),
filiere : 'A' ,
type_media : typeMedia ( c ),
vue : null ,
usages : [],
source : 'wikimedia_commons' ,
auteur : c . auteur ,
licence : c . licence ,
licence_url : c . licence_url ?? null ,
url_fichier : c . url_fichier ,
url_description : c . url_description ,
largeur : c . largeur ?? null ,
hauteur : c . hauteur ?? null ,
mime : c . mime ?? null ,
description : c . description ?? null ,
date_prise_de_vue : c . date_prise_de_vue ?? null ,
attribution_requise : ! SANS_ATTRIBUTION . has ( c . licence ),
partage_identique : AVEC_SA . has ( c . licence ),
usage_non_commercial : AVEC_NC . has ( c . licence ),
composite : false ,
valide : false ,
valide_par : null ,
valide_le : null ,
note_validation : null
}))
);
const entree = {
$schema : '../../../../fruits.schema.json' ,
id : e . id ,
categorie : e . categorie ,
2026-07-27 10:40:23 -04:00
noms : { ... e . noms , variantes_gcf : [] },
orthographe : {
norme : 'gwadloupeyen' ,
statut : QUESTIONS_KREYOL [ e . id ] ? 'en_discussion' : 'a_relire' ,
relu_par : null ,
question_ouverte : QUESTIONS_KREYOL [ e . id ] ?? null
},
2026-07-26 23:47:27 -04:00
taxon : {
scientifique : t . gbif . canonicalName ?? e . taxon . scientifique ,
famille : t . gbif . famille ?? e . taxon . famille ,
wikidata : t . wikidata . qid ,
gbif : t . gbif . usageKey ,
verifie_le : dateVerif ,
verifie_contre : [ 'wikidata' , 'gbif' ]
},
iles : e . iles ,
...( e . categorie === 'fruit' ? { saison_mois : e . saison_mois } : {}),
// Les cinq plantes du module « Pa touché » sortent à comestible: false.
// C'est le défaut prudent, pas la vérité complète : l'akée mûre et la pomme-cajou
// se mangent sous conditions strictes. La nuance vit dans danger.resume et doit
// être arbitrée par un humain avant publication (voir docs/a-arbitrer.md).
comestible : e . categorie === 'fruit' ? ( e . comestible ?? true ) : false ,
danger : normaliserDanger ( e . danger ),
indices : e . indices ?? {},
medias : candidats ,
2026-07-27 10:40:23 -04:00
audio : {
gcf : null ,
locuteur : null ,
source : null ,
licence : null ,
url_description : null ,
valide : false ,
note : null
},
2026-07-26 23:47:27 -04:00
sources_factuelles : [],
valide : false
};
if ( e . $comment ) entree . $comment = e . $comment ;
await writeFile ( chemin , JSON . stringify ( entree , null , '\t' ) + '\n' );
console . log ( `✔ ${ e . id } — ${ candidats . length } médias, ${ entree . taxon . wikidata } / ${ entree . taxon . gbif } ` );
ecrits ++ ;
}
console . log ( `\n ${ ecrits } entrée(s) écrite(s), ${ ignores } conservée(s).` );
}
main (). catch (( err ) => {
console . error ( '✘' , err . message );
process . exit ( 1 );
});