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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-audio.mjs — liste les enregistrements kréyòl manquants.
|
||||
*
|
||||
* Le brief §6 le demande explicitement : « prévois le contrat (audio.gcf, format .opus,
|
||||
* repli silencieux si le fichier manque) et un script de vérification listant les
|
||||
* enregistrements manquants. »
|
||||
*
|
||||
* Pas de synthèse vocale : ce script n'en fabrique aucune. Il produit une liste de courses
|
||||
* pour la séance d'enregistrement — une après-midi de terrain vaut dix heures de code.
|
||||
*
|
||||
* Ce script n'échoue jamais : un enregistrement manquant est un travail à faire,
|
||||
* pas un défaut de build. Le jeu doit se replier en silence.
|
||||
*/
|
||||
|
||||
import { readFile, readdir, writeFile, access } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const DATA = join(ROOT, 'src', 'lib', 'data', 'fruits');
|
||||
const STATIC = join(ROOT, 'static');
|
||||
|
||||
async function existe(p) {
|
||||
try {
|
||||
await access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fichiers = (await readdir(DATA)).filter((f) => f.endsWith('.json')).sort();
|
||||
const manquants = [];
|
||||
const absents = [];
|
||||
const presents = [];
|
||||
|
||||
for (const f of fichiers) {
|
||||
const e = JSON.parse(await readFile(join(DATA, f), 'utf8'));
|
||||
if (!e.audio.gcf) {
|
||||
manquants.push(e);
|
||||
continue;
|
||||
}
|
||||
// Le chemin est absolu depuis la racine du site : /audio/<id>.opus → static/audio/<id>.opus
|
||||
if (await existe(join(STATIC, e.audio.gcf))) presents.push(e);
|
||||
else absents.push(e);
|
||||
}
|
||||
|
||||
const l = [
|
||||
'# Enregistrements kréyòl à réaliser',
|
||||
'',
|
||||
'> Généré par `npm run check:audio`. Aucune synthèse vocale : ces noms doivent être',
|
||||
'> prononcés par une voix humaine locale (brief §6).',
|
||||
'',
|
||||
"Format attendu : `.opus` — un fichier par entrée, déposé dans `static/audio/<id>.opus`.",
|
||||
'',
|
||||
`## À enregistrer (${manquants.length})`,
|
||||
''
|
||||
];
|
||||
for (const e of manquants) l.push(`- [ ] **${e.noms.gcf}** — \`static/audio/${e.id}.opus\` _(${e.noms.fr ?? ''})_`);
|
||||
l.push('');
|
||||
if (absents.length) {
|
||||
l.push(`## Déclarés mais fichier introuvable (${absents.length})`, '');
|
||||
for (const e of absents) l.push(`- ⚠ \`${e.id}\` → \`${e.audio.gcf}\` absent de \`static/\``);
|
||||
l.push('');
|
||||
}
|
||||
if (presents.length) {
|
||||
l.push(`## En place (${presents.length})`, '');
|
||||
for (const e of presents) l.push(`- ✔ \`${e.id}\` — ${e.audio.locuteur ?? 'locuteur non renseigné'}`);
|
||||
l.push('');
|
||||
}
|
||||
|
||||
await writeFile(join(ROOT, 'docs', 'audio-a-enregistrer.md'), l.join('\n'));
|
||||
|
||||
console.log(`À enregistrer : ${manquants.length}`);
|
||||
console.log(`Introuvables : ${absents.length}`);
|
||||
console.log(`En place : ${presents.length}`);
|
||||
console.log('\n docs/audio-a-enregistrer.md');
|
||||
if (absents.length) console.log("\n⚠ Des chemins audio sont déclarés sans fichier : le jeu se repliera en silence.");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('✘', e.stack ?? e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user