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>
239 lines
7.5 KiB
JavaScript
239 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* test-porte.mjs — prouve que la porte se ferme.
|
||
*
|
||
* Une règle de sécurité qu'on n'a jamais vue refuser quoi que ce soit n'est pas une règle,
|
||
* c'est une intention. Ce test fabrique les cas interdits par le brief et vérifie que
|
||
* `validate.mjs` les rejette bien avec un code de sortie non nul.
|
||
*
|
||
* Chaque cas correspond à une phrase du brief, citée en regard.
|
||
*/
|
||
|
||
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
import { spawnSync } from 'node:child_process';
|
||
|
||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||
const BAC = join('tools', 'data', 'out', 'test-porte');
|
||
|
||
/** Applique une mutation à une copie profonde de l'entrée de référence. */
|
||
const muter = (base, fn) => {
|
||
const c = structuredClone(base);
|
||
fn(c);
|
||
return c;
|
||
};
|
||
|
||
/** Un média de référence, valide et utilisable comme épreuve. */
|
||
const mediaValide = (over = {}) => ({
|
||
id: 'photo-temoin',
|
||
filiere: 'A',
|
||
type_media: 'photo',
|
||
vue: 'fruit_entier',
|
||
usages: ['epreuve'],
|
||
source: 'wikimedia_commons',
|
||
auteur: 'Témoin',
|
||
licence: 'CC-BY-SA-4.0',
|
||
licence_url: null,
|
||
url_fichier: 'https://upload.wikimedia.org/temoin.jpg',
|
||
url_description: 'https://commons.wikimedia.org/wiki/File:temoin.jpg',
|
||
largeur: 800,
|
||
hauteur: 600,
|
||
mime: 'image/jpeg',
|
||
description: null,
|
||
date_prise_de_vue: null,
|
||
attribution_requise: true,
|
||
partage_identique: true,
|
||
usage_non_commercial: false,
|
||
composite: false,
|
||
valide: true,
|
||
valide_par: 'porteur',
|
||
valide_le: '2026-07-27',
|
||
note_validation: null
|
||
});
|
||
|
||
const CAS = [
|
||
{
|
||
nom: 'épreuve sur un média non validé',
|
||
regle: 'brief §2 — « Le build échoue si un média non validé est référencé par une épreuve. »',
|
||
muter: (e) => {
|
||
e.medias = [mediaValide({}), mediaValide()];
|
||
e.medias[0].id = 'photo-non-validee';
|
||
e.medias[0].valide = false;
|
||
e.medias[0].valide_par = null;
|
||
}
|
||
},
|
||
{
|
||
nom: 'épreuve sur un asset généré (filière B)',
|
||
regle: "brief §10 — « Aucune épreuve d'identification ne s'appuie sur un asset IA. »",
|
||
muter: (e) => {
|
||
e.medias = [mediaValide()];
|
||
e.medias[0].filiere = 'B';
|
||
e.medias[0].type_media = 'image_generee';
|
||
}
|
||
},
|
||
{
|
||
nom: 'épreuve sur une planche botanique',
|
||
regle: "brief §2 — filière A = photographie réelle ; une gravure documente mais n'entraîne pas la reconnaissance du réel.",
|
||
muter: (e) => {
|
||
e.medias = [mediaValide()];
|
||
e.medias[0].type_media = 'planche_botanique';
|
||
}
|
||
},
|
||
{
|
||
nom: 'épreuve portée par une plante dangereuse',
|
||
regle: 'brief §6 — « Pa touché : jamais chronométré, jamais scoré, jamais transformé en épreuve. »',
|
||
muter: (e) => {
|
||
e.categorie = 'plante_dangereuse';
|
||
e.danger = { niveau: 'critique', resume: 'Ne jamais toucher ni manger ce fruit.' };
|
||
delete e.saison_mois;
|
||
e.comestible = false;
|
||
e.medias = [mediaValide()];
|
||
}
|
||
},
|
||
{
|
||
nom: 'média à attribution sans auteur',
|
||
regle: 'brief §3 — « Récupérer auteur, licence, URL du fichier, URL de la page de description. »',
|
||
muter: (e) => {
|
||
e.medias = [mediaValide()];
|
||
e.medias[0].auteur = null;
|
||
}
|
||
},
|
||
{
|
||
nom: 'compositing sur un média en partage à l’identique',
|
||
regle: "ADR 0001 — le SA contamine les dérivés, le NC est incompatible avec le SA : aucun compositing.",
|
||
muter: (e) => {
|
||
e.medias = [mediaValide()];
|
||
e.medias[0].composite = true;
|
||
}
|
||
},
|
||
{
|
||
nom: 'entrée publiée avec une seule photo validée',
|
||
regle: 'brief §8 — « ≥ 2 photos Commons par fruit avec auteur, licence et URL. »',
|
||
muter: (e) => {
|
||
e.medias = [mediaValide()];
|
||
e.orthographe = { norme: 'gwadloupeyen', statut: 'relu', relu_par: 'relecteur', question_ouverte: null };
|
||
e.valide = true;
|
||
}
|
||
},
|
||
{
|
||
nom: 'entrée publiée sans relecture kréyòl',
|
||
regle: "brief §5 — « L'orthographe kréyòl n'est pas de ton ressort. »",
|
||
muter: (e) => {
|
||
e.medias = [mediaValide(), muter(mediaValide(), (m) => (m.id = 'photo-temoin-2'))];
|
||
e.valide = true;
|
||
}
|
||
},
|
||
{
|
||
nom: 'validation anonyme',
|
||
regle: 'porte du projet — une validation sans nom n’engage personne.',
|
||
muter: (e) => {
|
||
e.medias = [mediaValide()];
|
||
e.medias[0].valide_par = null;
|
||
}
|
||
},
|
||
{
|
||
nom: 'doute orthographique signalé sans question posée',
|
||
regle: 'ADR 0005 — on ne signale pas un doute sans dire lequel, sinon on décourage au lieu d’appeler.',
|
||
muter: (e) => {
|
||
e.orthographe = { norme: 'gwadloupeyen', statut: 'en_discussion', relu_par: null, question_ouverte: null };
|
||
}
|
||
},
|
||
{
|
||
nom: 'enregistrement sans locuteur crédité',
|
||
regle: 'brief §6 et LICENSE-CONTENT §4 — le locuteur conserve ses droits voisins et est crédité nominativement.',
|
||
muter: (e) => {
|
||
e.audio = {
|
||
gcf: '/audio/kowosol.opus',
|
||
locuteur: null,
|
||
source: 'contributeur',
|
||
licence: 'CC-BY-SA-4.0',
|
||
url_description: null,
|
||
valide: false,
|
||
note: null
|
||
};
|
||
}
|
||
},
|
||
{
|
||
nom: 'enregistrement externe sans page d’origine',
|
||
regle: 'brief §3 — toute ressource tierce porte sa source vérifiable.',
|
||
muter: (e) => {
|
||
e.audio = {
|
||
gcf: '/audio/kowosol.opus',
|
||
locuteur: 'Palé Kréyol',
|
||
source: 'lingualibre',
|
||
licence: 'CC-BY-SA-4.0',
|
||
url_description: null,
|
||
valide: false,
|
||
note: null
|
||
};
|
||
}
|
||
},
|
||
{
|
||
nom: 'variante orthographique identique au nom principal',
|
||
regle: 'ADR 0005 — une graphie confirmée est une corroboration, pas une variante : ne pas gonfler le doute.',
|
||
muter: (e) => {
|
||
e.noms.variantes_gcf = [{ graphie: e.noms.gcf, source: 'un locuteur', ile: 'gp', note: null }];
|
||
}
|
||
}
|
||
];
|
||
|
||
/** Cas témoin : doit passer. Un test qui échoue toujours ne prouve rien non plus. */
|
||
const TEMOIN = {
|
||
nom: 'TÉMOIN — entrée correcte, doit passer',
|
||
attendu: 0,
|
||
muter: (e) => {
|
||
e.medias = [mediaValide(), muter(mediaValide(), (m) => (m.id = 'photo-temoin-2'))];
|
||
e.orthographe = { norme: 'gwadloupeyen', statut: 'relu', relu_par: 'relecteur', question_ouverte: null };
|
||
e.valide = true;
|
||
}
|
||
};
|
||
|
||
async function jouer(cas, base, i) {
|
||
const dir = join(BAC, `cas-${i}`);
|
||
await mkdir(join(ROOT, dir), { recursive: true });
|
||
await writeFile(join(ROOT, dir, `${base.id}.json`), JSON.stringify(muter(base, cas.muter), null, '\t'));
|
||
|
||
const r = spawnSync(process.execPath, [join(ROOT, 'tools', 'data', 'validate.mjs')], {
|
||
cwd: ROOT,
|
||
env: { ...process.env, KI_FWI_DATA: dir },
|
||
encoding: 'utf8'
|
||
});
|
||
|
||
const attendu = cas.attendu ?? 1;
|
||
const obtenu = r.status;
|
||
const ok = obtenu === attendu;
|
||
console.log(`${ok ? '✔' : '✘'} ${cas.nom}`);
|
||
if (cas.regle) console.log(` ${cas.regle}`);
|
||
if (!ok) {
|
||
console.log(` attendu code ${attendu}, obtenu ${obtenu}`);
|
||
console.log((r.stderr || r.stdout).split('\n').slice(-8).join('\n'));
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
async function main() {
|
||
const base = JSON.parse(await readFile(join(ROOT, 'src/lib/data/fruits/kowosol.json'), 'utf8'));
|
||
await rm(join(ROOT, BAC), { recursive: true, force: true });
|
||
|
||
console.log('\nTest de la porte de validation\n');
|
||
const resultats = [];
|
||
resultats.push(await jouer(TEMOIN, base, 0));
|
||
for (const [i, cas] of CAS.entries()) resultats.push(await jouer(cas, base, i + 1));
|
||
|
||
await rm(join(ROOT, BAC), { recursive: true, force: true });
|
||
|
||
const echecs = resultats.filter((r) => !r).length;
|
||
console.log(`\n${resultats.length - echecs}/${resultats.length} cas conformes.`);
|
||
if (echecs) {
|
||
console.error(`✘ ${echecs} cas non couvert(s) : la porte laisse passer ce qu'elle devrait refuser.`);
|
||
process.exit(1);
|
||
}
|
||
console.log('✔ La porte refuse tout ce que le brief interdit.');
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error('✘', e.stack ?? e.message);
|
||
process.exit(1);
|
||
});
|