126 lines
5.1 KiB
JavaScript
126 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
||||
|
|
/**
|
|||
|
|
* verify-candidats.mjs — vérifie la taxonomie des espèces candidates à l'extension.
|
|||
|
|
*
|
|||
|
|
* La session 5 doit porter le dataset de 8 à 24 fruits. Ce que je peux faire : vérifier
|
|||
|
|
* les noms scientifiques, les familles, les Q-id et les clés GBIF contre les référentiels.
|
|||
|
|
*
|
|||
|
|
* Ce que je ne peux PAS faire : donner leur nom kréyòl. Le nom kréyòl est le nom
|
|||
|
|
* PRINCIPAL de l'entrée (ADR 0005) et aucun agent de code n'en écrit. Le tableau produit
|
|||
|
|
* comporte donc une colonne vide, et c'est le point du document.
|
|||
|
|
*
|
|||
|
|
* Sortie : docs/candidats-24-fruits.md
|
|||
|
|
*/
|
|||
|
|
import { writeFile } from 'node:fs/promises';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
import { dirname, join } from 'node:path';
|
|||
|
|
|
|||
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|||
|
|
const UA = 'ki-fwi-dataset/0.1 (https://o-k-i.net ; projet educatif ORGANISATION KA INTERNATIONALE)';
|
|||
|
|
|
|||
|
|
/** Espèces couramment rencontrées aux Antilles. Le nom français sert de repère, pas de référence. */
|
|||
|
|
const CANDIDATS = [
|
|||
|
|
['Mangifera indica', 'mangue'],
|
|||
|
|
['Carica papaya', 'papaye'],
|
|||
|
|
['Musa acuminata', 'banane'],
|
|||
|
|
['Ananas comosus', 'ananas'],
|
|||
|
|
['Cocos nucifera', 'coco'],
|
|||
|
|
['Annona reticulata', 'cachiman cœur-de-bœuf'],
|
|||
|
|
['Passiflora laurifolia', 'pomme-liane'],
|
|||
|
|
['Passiflora edulis', 'fruit de la passion'],
|
|||
|
|
['Averrhoa carambola', 'carambole'],
|
|||
|
|
['Tamarindus indica', 'tamarin'],
|
|||
|
|
['Manilkara zapota', 'sapotille'],
|
|||
|
|
['Spondias mombin', 'mombin'],
|
|||
|
|
['Persea americana', 'avocat'],
|
|||
|
|
['Citrus sinensis', 'orange'],
|
|||
|
|
['Malpighia emarginata', 'cerise des Antilles'],
|
|||
|
|
['Artocarpus heterophyllus', 'jacquier']
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|||
|
|
|
|||
|
|
async function getJson(url, params) {
|
|||
|
|
const u = new URL(url);
|
|||
|
|
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
|
|||
|
|
const res = await fetch(u, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
|||
|
|
if (!res.ok) throw new Error(`${res.status}`);
|
|||
|
|
return res.json();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function wikidata(noms) {
|
|||
|
|
const values = noms.map((n) => JSON.stringify(n)).join(' ');
|
|||
|
|
const query = `SELECT ?taxon ?name ?fam WHERE {
|
|||
|
|
VALUES ?name { ${values} }
|
|||
|
|
?taxon wdt:P225 ?name .
|
|||
|
|
OPTIONAL { ?taxon wdt:P171+ ?f . ?f wdt:P105 wd:Q35409 ; wdt:P225 ?fam . }
|
|||
|
|
}`;
|
|||
|
|
const u = new URL('https://query.wikidata.org/sparql');
|
|||
|
|
u.searchParams.set('query', query);
|
|||
|
|
const res = await fetch(u, { headers: { 'User-Agent': UA, Accept: 'application/sparql-results+json' } });
|
|||
|
|
const json = await res.json();
|
|||
|
|
const m = new Map();
|
|||
|
|
for (const b of json.results.bindings) {
|
|||
|
|
m.set(b.name.value, { qid: b.taxon.value.split('/').pop(), famille: b.fam?.value ?? null });
|
|||
|
|
}
|
|||
|
|
return m;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resultats = [];
|
|||
|
|
const wd = await wikidata(CANDIDATS.map(([s]) => s));
|
|||
|
|
|
|||
|
|
for (const [sci, fr] of CANDIDATS) {
|
|||
|
|
process.stdout.write(`→ ${sci}… `);
|
|||
|
|
const g = await getJson('https://api.gbif.org/v1/species/match', { name: sci, strict: 'false' });
|
|||
|
|
const w = wd.get(sci) ?? null;
|
|||
|
|
const ecarts = [];
|
|||
|
|
if (!w) ecarts.push('Wikidata : pas de P225 exact');
|
|||
|
|
if (g.status !== 'ACCEPTED') ecarts.push(`GBIF : ${g.status} → ${g.canonicalName}`);
|
|||
|
|
if (w?.famille && g.family && w.famille !== g.family) ecarts.push(`familles divergentes : ${w.famille} / ${g.family}`);
|
|||
|
|
console.log(g.status ?? 'AUCUN', ecarts.length ? `⚠ ${ecarts.join(' · ')}` : '');
|
|||
|
|
resultats.push({ sci, fr, w, g, ecarts });
|
|||
|
|
await pause(250);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const l = [
|
|||
|
|
'# 16 espèces candidates — taxonomie vérifiée, noms kréyòl manquants',
|
|||
|
|
'',
|
|||
|
|
`> Généré par \`npm run verify:candidats\` le ${new Date().toISOString().slice(0, 10)}.`,
|
|||
|
|
'',
|
|||
|
|
'La session 5 doit porter le dataset de 8 à 24 fruits. **La taxonomie est vérifiable par',
|
|||
|
|
'machine, le nom kréyòl ne l’est pas.**',
|
|||
|
|
'',
|
|||
|
|
'Le nom kréyòl est le nom principal de chaque entrée (ADR 0005), et aucun agent de code',
|
|||
|
|
'n’en écrit. La colonne de droite est donc vide, et c’est tout l’objet de ce document :',
|
|||
|
|
'il ne manque que ce qu’une machine ne peut pas produire.',
|
|||
|
|
'',
|
|||
|
|
'| # | nom scientifique | famille | Wikidata | GBIF | français | **nom kréyòl** |',
|
|||
|
|
'|---|---|---|---|---|---|---|'
|
|||
|
|
];
|
|||
|
|
resultats.forEach((r, i) => {
|
|||
|
|
l.push(
|
|||
|
|
`| ${i + 1} | *${r.g.canonicalName ?? r.sci}* | ${r.g.family ?? '—'} | ${r.w?.qid ?? '—'} | ${r.g.usageKey ?? '—'} | ${r.fr} | |`
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const avecEcart = resultats.filter((r) => r.ecarts.length);
|
|||
|
|
l.push('', `**${resultats.length - avecEcart.length} / ${resultats.length}** vérifiés sans écart.`, '');
|
|||
|
|
if (avecEcart.length) {
|
|||
|
|
l.push('## Écarts relevés', '');
|
|||
|
|
for (const r of avecEcart) l.push(`- *${r.sci}* — ${r.ecarts.join(' · ')}`);
|
|||
|
|
l.push('');
|
|||
|
|
}
|
|||
|
|
l.push(
|
|||
|
|
'## Comment compléter',
|
|||
|
|
'',
|
|||
|
|
'Remplir la colonne « nom kréyòl » dans la norme gwadloupéyenne, puis ajouter les entrées',
|
|||
|
|
'à `fruits.seed.json` et rejouer `npm run verify:taxa && npm run build:dataset`.',
|
|||
|
|
'',
|
|||
|
|
'Chaque entrée ajoutée sortira en `orthographe.statut: "a_relire"` et',
|
|||
|
|
'`valide: false` — la porte ne s’ouvre pas plus grand parce que le dataset grandit.',
|
|||
|
|
''
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await writeFile(join(ROOT, 'docs', 'candidats-24-fruits.md'), l.join('\n'));
|
|||
|
|
console.log(`\n✔ docs/candidats-24-fruits.md — ${resultats.length} candidats, ${avecEcart.length} avec écart.`);
|