diff --git a/scripts/test-outils-data.mjs b/scripts/test-outils-data.mjs new file mode 100644 index 0000000..5b2f870 --- /dev/null +++ b/scripts/test-outils-data.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/** + * Test des données de /outils (B6-M1). + * Garde-fou pour les ajouts futurs (à la main, par M3, ou par un pipe n8n) : + * fiches de l'onglet « Partir de soi », ressources, purge des liens litigieux, URL en https. + * Charge le VRAI utils/echelles.ts via jiti (même patron que test-echelles-nav.mjs). + * Usage : node scripts/test-outils-data.mjs + */ + +import { createJiti } from 'jiti' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const jiti = createJiti(import.meta.url) +const { ECHELLES } = await jiti.import(join(__dirname, '..', 'utils', 'echelles.ts')) + +const lire = (nom) => JSON.parse(readFileSync(join(__dirname, '..', 'public', 'data', nom), 'utf8')) +const outils = lire('outils.json') +const fmhy = lire('fmhy-curated.json') + +const LISTE_NOIRE = [ + 'annas-archive', 'z-lib', 'libgen', 'sci-hub', 'mobilism', 'zone-ebook', 'rivestream', 'cineby', + 'flixer', 'miruro', 'monochrome.tf', 'lucida', 'spotx', 'fitgirl', 'steamrip', '1337x', +] +const FORMATS = ['film', 'vidéo', 'podcast', 'guide PDF', 'livret PDF', 'appli', 'retraite', 'atelier', 'formation', 'association', 'fiches', 'événement', 'récit'] +const ACCES = ['libre', 'don libre', 'gratuit avec inscription', 'prix libre', 'financé selon région'] +const SOURCES = ['inventaire S 27/09', 'outils.json (avant B6)'] + +let n = 0 +const echecs = [] +function verifie(ok, message) { + n++ + if (!ok) echecs.push(message) +} + +/** Parcourt un JSON à tous les niveaux : (chemin, clé, valeur) pour chaque chaîne. */ +function chaines(valeur, chemin, visiteur) { + if (typeof valeur === 'string') visiteur(chemin, valeur) + else if (Array.isArray(valeur)) valeur.forEach((v, i) => chaines(v, `${chemin}[${i}]`, visiteur)) + else if (valeur && typeof valeur === 'object') for (const [k, v] of Object.entries(valeur)) chaines(v, `${chemin}.${k}`, visiteur) +} + +// ── Fiches ──────────────────────────────────────────────────── +const p = outils.partir_de_soi +const fiches = p?.fiches ?? [] +const mots = ECHELLES.S.mots +verifie(outils.version === 2, 'version doit valoir 2') +verifie(fiches.length === 7, `7 fiches attendues, ${fiches.length} trouvées`) +verifie(new Set(fiches.map((f) => f.id)).size === fiches.length, 'ids de fiches non uniques') +verifie( + JSON.stringify(fiches.map((f) => f.mot)) === JSON.stringify(mots), + `mots des fiches ≠ ECHELLES.S.mots (dans l'ordre) : ${JSON.stringify(fiches.map((f) => f.mot))}`, +) +const idsFiches = new Set(fiches.map((f) => f.id)) + +// ── Ressources ──────────────────────────────────────────────── +const ressources = p?.ressources ?? [] +const idsRes = new Set() +const DATE = /^\d{4}-\d{2}-\d{2}$/ +for (const r of ressources) { + const nom = `ressource « ${r.id ?? r.titre ?? '?'} »` + verifie(typeof r.id === 'string' && r.id && !idsRes.has(r.id), `${nom} : id manquant ou en double`) + idsRes.add(r.id) + verifie(typeof r.titre === 'string' && r.titre, `${nom} : titre manquant`) + verifie(FORMATS.includes(r.format), `${nom} : format « ${r.format} » hors liste`) + verifie(ACCES.includes(r.acces), `${nom} : accès « ${r.acces} » hors liste`) + verifie(typeof r.pourquoi === 'string' && r.pourquoi, `${nom} : pourquoi manquant`) + verifie(Array.isArray(r.fiches) && r.fiches.length > 0, `${nom} : fiches vide`) + verifie((r.fiches ?? []).every((id) => idsFiches.has(id)), `${nom} : fiche inconnue dans ${JSON.stringify(r.fiches)}`) + verifie(SOURCES.includes(r.source), `${nom} : source « ${r.source} » hors liste`) + verifie(['socle', 'actualite'].includes(r.couche), `${nom} : couche « ${r.couche} »`) + verifie(['vivant', 'dormant', 'archive'].includes(r.statut), `${nom} : statut « ${r.statut} »`) + verifie(DATE.test(r.verifie_le ?? ''), `${nom} : verifie_le « ${r.verifie_le} » n'est pas AAAA-MM-JJ`) +} + +// ── Fiche vide ⇔ sans ressource ─────────────────────────────── +for (const f of fiches) { + const nb = ressources.filter((r) => r.fiches?.includes(f.id)).length + if (nb === 0) verifie(typeof f.vide === 'string' && f.vide, `fiche « ${f.id} » sans ressource : \`vide\` doit être renseigné`) + else verifie(f.vide == null, `fiche « ${f.id} » a ${nb} ressource(s) : \`vide\` doit être nul`) +} + +// ── URL : https partout, rien de la liste noire ─────────────── +for (const [fichier, donnees] of [['outils.json', outils], ['fmhy-curated.json', fmhy]]) { + chaines(donnees, fichier, (chemin, s) => { + const cle = chemin.split('.').pop().replace(/\[\d+\]$/, '') + const estUrl = /^https?:\/\//i.test(s) || ['url', 'urls', 'lien', 'avertissement_lien'].includes(cle) + // `lien` de contribution est un chemin interne (/proposer…) : seul le préfixe http est contrôlé. + if (/^https?:\/\//i.test(s)) { + verifie(s.startsWith('https://'), `${chemin} : URL non https (${s})`) + const bas = s.toLowerCase() + const hit = LISTE_NOIRE.find((d) => bas.includes(d)) + verifie(!hit, `${chemin} : domaine de la liste noire « ${hit} » (${s})`) + } else if (estUrl && cle !== 'lien' && s) { + verifie(false, `${chemin} : URL sans https (${s})`) + } + }) +} + +// Liste noire aussi dans les noms/titres (un lien retiré ne doit pas survivre en texte). +chaines({ outils, fmhy }, 'données', (chemin, s) => { + const bas = s.toLowerCase() + verifie(!bas.includes('tantra'), `${chemin} : contient « tantra »`) + verifie(!bas.includes('the nest'), `${chemin} : contient « The Nest »`) + const hit = ['sci-hub', 'z-library', 'library genesis', "anna's archive", 'fitgirl', 'steamrip', '1337x'].find((d) => bas.includes(d)) + verifie(!hit, `${chemin} : contient « ${hit} »`) +}) + +// ── Structure minimale des autres blocs ─────────────────────── +verifie(Array.isArray(outils.conception?.outils) && outils.conception.outils.length > 0, 'conception.outils vide') +verifie( + (outils.conception?.outils ?? []).every((o) => o.mention && DATE.test(o.verifie_le ?? '')), + 'conception.outils : `mention` et `verifie_le` obligatoires', +) +verifie(Array.isArray(outils.open_source?.rubriques) && outils.open_source.rubriques.every((r) => r.liens?.length > 0), 'open_source.rubriques : une rubrique vide doit disparaître') +verifie(!!outils.contribution?.texte && !!outils.contribution?.lien && !!outils.contribution?.cta, 'contribution incomplète') + +if (echecs.length) { + for (const e of echecs) console.log(`FAIL ${e}`) + console.log(`\n${n - echecs.length}/${n} vérifications OK — ${echecs.length} échec(s)`) + process.exit(1) +} +console.log(`${n} vérifications OK`)