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

239 lines
7.5 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
/**
* 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 à lidentique',
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 nengage 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 dappeler.',
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 dorigine',
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);
});