Files
OKIandClaude Opus 5 4f78321195 feat(kreyol): ouvrir la relecture et la prononciation aux locuteurs
Amendement de l'ADR 0005 : la relecture ne repose plus sur un relecteur
unique. La porte ne s'abaisse pas — l'ouverture porte sur qui peut
proposer, jamais sur qui peut valider.

La communauté existait déjà
- Lingua Libre compte 456 enregistrements en kréyòl gwadloupéyen, versés
  sur Commons sous licence libre.
- Trois de nos treize noms y figuraient déjà — gouyav, kénèt, zikak — en
  CC BY-SA 4.0 et AVEC EXACTEMENT NOTRE GRAPHIE. Trois entrées sortent du
  doute par corroboration externe, sans que le projet décide quoi que ce
  soit. La question n'était pas qui trancherait, mais d'aller regarder.
- En regard : 0 lexème Wikidata en gcf, 0 étiquette gcf sur les 13 items.

Modèle de données
- orthographe.statut : a_relire | en_discussion | relu. Un doute signalé
  sans question posée est refusé par le validateur — on ne décourage pas
  le lecteur, on l'appelle. 4 entrées en discussion : ake, medisinye,
  lorye-jonn, pom-dlo.
- noms.variantes_gcf : les usages proposés sont conservés avec leur source
  et leur île, jamais écrasés. Une graphie identique au nom principal est
  refusée comme variante : c'est une corroboration, pas un désaccord.
- audio : source, licence, url_description, locuteur crédité, valide.
  check:audio interroge Lingua Libre et trouve seul les prises existantes.

Porte : 14/14 (5 cas ajoutés sur la langue et la prononciation).

Pis-aller assumé : les 4 questions sont rédigées en français. Les écrire
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. Leur traduction est la première
contribution demandée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:40:23 -04:00

184 lines
7.9 KiB
JavaScript
Raw Permalink 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.
#!/usr/bin/env node
/**
* validate.mjs — LA PORTE. Ce script est la règle n°1 du projet écrite en code.
*
* Le brief §2 l'exige mot pour mot : « Le build échoue si un média non validé est
* référencé par une épreuve. Écris ce contrôle comme un script de build, pas comme
* une consigne. » Ce fichier est ce contrôle.
*
* Il vérifie deux couches :
* 1. la forme, contre fruits.schema.json (Ajv) ;
* 2. les règles que le schéma ne peut pas exprimer — filières, licences, sécurité.
*
* Sortie non nulle = build refusé. Aucune de ces règles ne se contourne par un flag.
*/
import { readFile, readdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join, basename } from 'node:path';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
/** Surchargeable pour les tests de la porte (tools/data/test-porte.mjs). */
const DATA = process.env.KI_FWI_DATA ? join(ROOT, process.env.KI_FWI_DATA) : join(ROOT, 'src', 'lib', 'data', 'fruits');
/** Usages qui servent à identifier une espèce. Ce sont eux qui déclenchent la porte. */
const USAGES_IDENTIFIANTS = new Set(['epreuve', 'indice']);
const erreurs = [];
const alertes = [];
const err = (fichier, msg) => erreurs.push(`${fichier}${msg}`);
const warn = (fichier, msg) => alertes.push(`${fichier}${msg}`);
async function main() {
const schema = JSON.parse(await readFile(join(ROOT, 'fruits.schema.json'), 'utf8'));
const ajv = new Ajv2020({ allErrors: true, strict: false });
addFormats(ajv);
const valider = ajv.compile(schema);
const fichiers = (await readdir(DATA)).filter((f) => f.endsWith('.json')).sort();
if (!fichiers.length) {
console.error('✘ Aucune entrée dans src/lib/data/fruits/.');
process.exit(1);
}
const vus = new Map();
const entrees = [];
let nbMedias = 0;
let nbMediasValides = 0;
let nbNC = 0;
for (const f of fichiers) {
const brut = await readFile(join(DATA, f), 'utf8');
let e;
try {
e = JSON.parse(brut);
} catch (ex) {
err(f, `JSON illisible : ${ex.message}`);
continue;
}
// ── Couche 1 : la forme
if (!valider(e)) {
for (const d of valider.errors ?? []) err(f, `schéma : ${d.instancePath || '/'} ${d.message}`);
continue;
}
// ── Couche 2 : les règles métier
if (e.id !== basename(f, '.json')) err(f, `id « ${e.id} » ≠ nom de fichier`);
if (vus.has(e.id)) err(f, `id « ${e.id} » déjà utilisé par ${vus.get(e.id)}`);
vus.set(e.id, f);
const idsMedias = new Set();
for (const m of e.medias) {
nbMedias++;
if (m.valide) nbMediasValides++;
if (m.usage_non_commercial) nbNC++;
if (idsMedias.has(m.id)) err(f, `média « ${m.id} » en double`);
idsMedias.add(m.id);
const identifiant = m.usages.some((u) => USAGES_IDENTIFIANTS.has(u));
// LA PORTE — brief §2 et §10.
if (identifiant) {
if (m.filiere !== 'A')
err(f, `média « ${m.id} » sert à identifier mais relève de la filière ${m.filiere} : une image générée n'enseigne pas le réel`);
if (m.type_media !== 'photo')
err(f, `média « ${m.id} » sert à identifier mais n'est pas une photo (${m.type_media})`);
if (!m.valide)
err(f, `média « ${m.id} » sert à identifier mais n'est pas validé — aucun humain n'a confirmé qu'il montre bien ${e.taxon.scientifique}`);
if (!m.vue) err(f, `média « ${m.id} » sert à identifier mais sa vue n'est pas renseignée`);
}
// Cohérence des filières
if (m.filiere === 'B' && m.type_media !== 'image_generee')
err(f, `média « ${m.id} » : filière B impose type_media = image_generee`);
if (m.filiere === 'B' && m.usages.some((u) => u !== 'decor'))
err(f, `média « ${m.id} » : un média de filière B ne peut servir qu'au décor`);
if (m.filiere === 'A' && m.type_media === 'image_generee')
err(f, `média « ${m.id} » : filière A n'accepte aucune image générée`);
// Licences — brief §3
if (m.attribution_requise && !m.auteur)
err(f, `média « ${m.id} » : licence ${m.licence} à attribution, auteur manquant`);
if (m.composite && (m.partage_identique || m.usage_non_commercial))
err(f, `média « ${m.id} » : compositing interdit sur un média SA ou NC (conflit de licence, ADR 0001)`);
if (m.valide && !m.valide_par)
err(f, `média « ${m.id} » : validé sans nom de validateur — une validation anonyme n'en est pas une`);
// Signalement, pas blocage : l'ADR 0001 accepte le NC contre un engagement non commercial.
if (m.usage_non_commercial && m.source === 'wikimedia_commons')
warn(f, `média « ${m.id} » : NC annoncé sur une source Commons — vérifier, Commons n'héberge pas de NC`);
}
// Sécurité enfants — brief §6 : « Pa touché » n'est jamais une épreuve, jamais scoré.
if (e.categorie === 'plante_dangereuse') {
if (!e.danger) err(f, 'plante dangereuse sans bloc danger');
for (const m of e.medias)
if (m.usages.includes('epreuve'))
err(f, `média « ${m.id} » : une plante dangereuse ne peut pas porter d'épreuve — le module Pa touché n'est ni chronométré ni scoré`);
}
// Conditions de publication d'une entrée
if (e.valide) {
const photos = e.medias.filter((m) => m.valide && m.filiere === 'A' && m.type_media === 'photo');
if (photos.length < 2)
err(f, `entrée déclarée valide avec ${photos.length} photo(s) validée(s) — le brief §8 en exige au moins deux`);
if (e.orthographe.statut !== 'relu')
err(f, "entrée déclarée valide alors que l'orthographe kréyòl n'est pas relue (ADR 0005)");
} else {
warn(f, 'entrée non encore validée pour publication');
}
// Langue — ADR 0005. On ne tranche pas l'orthographe, mais on ne bâcle pas la question.
if (e.orthographe.statut === 'en_discussion' && !e.orthographe.question_ouverte)
err(f, "orthographe « en discussion » sans question posée — on ne signale pas un doute sans dire lequel");
if (e.orthographe.statut === 'relu' && !e.orthographe.relu_par)
err(f, 'orthographe déclarée relue sans nom de relecteur');
for (const v of e.noms.variantes_gcf ?? [])
if (v.graphie === e.noms.gcf)
err(f, `variante « ${v.graphie} » identique au nom principal — c'est une corroboration, pas une variante`);
// Prononciation — brief §6 : voix humaine, jamais de synthèse.
if (e.audio.gcf) {
if (!e.audio.locuteur) err(f, 'enregistrement sans locuteur crédité — il conserve ses droits voisins');
if (!e.audio.licence) err(f, 'enregistrement sans licence déclarée');
if (e.audio.source && e.audio.source !== 'porteur' && !e.audio.url_description)
err(f, `enregistrement de source « ${e.audio.source} » sans page d'origine`);
if (!e.audio.valide) warn(f, 'enregistrement présent mais non validé — le jeu se repliera en silence');
} else {
warn(f, 'aucun enregistrement kréyòl — le jeu devra se replier en silence');
}
if (e.orthographe.statut === 'a_relire') warn(f, 'orthographe kréyòl à relire');
if (e.orthographe.statut === 'en_discussion') warn(f, 'orthographe kréyòl en discussion — question posée aux locuteurs');
entrees.push(e);
}
// ── Rapport
console.log(`\nEntrées : ${entrees.length}`);
console.log(`Médias : ${nbMedias} (${nbMediasValides} validés, ${nbNC} sous clause NC)`);
console.log(`Publiables : ${entrees.filter((e) => e.valide).length}`);
if (alertes.length) {
console.log(`\n⚠ ${alertes.length} alerte(s) — n'empêchent pas le build :`);
for (const a of alertes) console.log(` · ${a}`);
}
if (erreurs.length) {
console.error(`\n✘ ${erreurs.length} erreur(s) — build refusé :`);
for (const e of erreurs) console.error(` · ${e}`);
process.exit(1);
}
console.log('\n✔ Dataset conforme. Aucune épreuve ne sappuie sur un média non validé.');
}
main().catch((e) => {
console.error('✘', e.stack ?? e.message);
process.exit(1);
});