Session 0 du plan — aucun code applicatif. Cadre - 5 ADR : licences, Phaser 4 hors budget initial, frontière Svelte/Phaser, filières A/B des assets, kréyòl langue principale (norme gwadloupéyenne). - LICENSE (AGPL-3.0-or-later, texte canonique gnu.org), LICENSE-CONTENT (CC BY-SA 4.0). Le jeu est une œuvre collective : aucun compositing sur un média SA ou NC, contrainte vérifiée par script. - Audit des 7 sources avec endpoints réellement appelés. TRAMIL vérifié en direct : « all rights reserved » et page de mentions légales en 404. iNaturalist mesuré à 93 % de CC BY-NC sur un échantillon de 305 photos. Outillage (tools/data/, hors bundle, zéro appel réseau au runtime) - verify:taxa, ingest:commons, build:dataset, validate:data, test:porte, build:credits, check:audio. - validate.mjs est la porte exigée par le brief §2 : le build échoue si une épreuve s'appuie sur un média non validé, non photographique ou de filière B, sur une attribution sans auteur, sur un compositing SA/NC, ou sur une entrée publiée sans relecture kréyòl. test:porte prouve les 9 refus + 1 témoin. Données - 13 entrées (8 fruits + 5 plantes du module Pa touché) vérifiées contre Wikidata ET GBIF : 13/13 ACCEPTED, 0 divergence avec la graine. - 130 candidats Commons sous licence libre, 39 pré-inscrits, 0 validé — aucun agent ne peut valider un média. Écarts signalés, non corrigés en silence - Le champ danger de la graine a deux formes (note vs resume) : normalisé. - 3 identifiants divergent du nom kréyòl au-delà des accents : non tranchés. - comestible:false sur akée et pomme-cajou : sous-estimation prudente assumée. - Le mancenillier n'a aucune photographie moderne dans ses 3 premiers candidats — l'entrée la plus critique est la moins bien servie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
6.1 KiB
JavaScript
195 lines
6.1 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' };
|
||
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;
|
||
}
|
||
}
|
||
];
|
||
|
||
/** 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' };
|
||
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);
|
||
});
|