Files
ki-fwi/tools/data/test-porte.mjs
T
OKIandClaude Opus 5 945a524b44 feat(session-0): cadre juridique, porte de validation et dataset vérifié
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>
2026-07-26 23:47:27 -04:00

195 lines
6.1 KiB
JavaScript
Raw 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' };
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;
}
}
];
/** 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);
});