#!/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') // ── Open source : sélection (B6-M4) ─────────────────────────── // Chaque lien de la sélection est vérifié par une personne (licence, date) ; la liste noire // piraterie s'applique déjà à toute chaîne d'outils.json (voir plus haut), sélection comprise. const sel = outils.open_source?.selection verifie(!!sel && typeof sel.intention === 'string' && sel.intention, 'open_source.selection : intention manquante') verifie(Array.isArray(sel?.rubriques) && sel.rubriques.length >= 1, 'open_source.selection : au moins une rubrique attendue') const idsRub = new Set() for (const r of sel?.rubriques ?? []) { const rub = `sélection « ${r.id ?? r.titre ?? '?'} »` verifie(typeof r.id === 'string' && r.id && !idsRub.has(r.id), `${rub} : id manquant ou en double`) idsRub.add(r.id) verifie(typeof r.titre === 'string' && r.titre, `${rub} : titre manquant`) verifie(Array.isArray(r.liens) && r.liens.length > 0, `${rub} : rubrique vide`) for (const l of r.liens ?? []) { const nom = `${rub} › ${l.nom ?? '?'}` verifie(typeof l.nom === 'string' && l.nom, `${nom} : nom manquant`) verifie(typeof l.url === 'string' && l.url.startsWith('https://'), `${nom} : url https manquante (${l.url})`) verifie(typeof l.desc === 'string' && l.desc, `${nom} : desc manquante`) verifie(typeof l.licence === 'string' && l.licence, `${nom} : licence manquante`) verifie(DATE.test(l.verifie_le ?? ''), `${nom} : verifie_le « ${l.verifie_le} » n'est pas AAAA-MM-JJ`) const bas = (l.url ?? '').toLowerCase() const hit = LISTE_NOIRE.find((d) => bas.includes(d)) verifie(!hit, `${nom} : domaine de la liste noire « ${hit} »`) } } // Aucun doublon d'URL dans tout open_source (sélection + recommandés + rubriques fmhy), à la barre finale et à la casse près. const norm = (u) => u.toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/+$/, '') const vues = new Map() const urlsOS = [ ...(sel?.rubriques ?? []).flatMap((r) => (r.liens ?? []).map((l) => [`sélection › ${l.nom}`, l.url])), ...(outils.open_source?.recommandes ?? []).filter((o) => o.url).map((o) => [`recommandé › ${o.titre}`, o.url]), ...(outils.open_source?.rubriques ?? []).flatMap((r) => (r.liens ?? []).map((l) => [`fmhy › ${l.nom}`, l.url])), ] for (const [qui, url] of urlsOS) { const k = norm(url) verifie(!vues.has(k), `open_source : URL en double « ${url} » (${vues.get(k)} et ${qui})`) vues.set(k, qui) } 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`)