Files
nav-carte/scripts/test-outils-data.mjs
T
Jules NenyandClaude Sonnet 43ae065e26 feat(outils): sélection open source, sommaire d'ancres et garde-fous (B6-M4)
28 outils vérifiés en 4 rubriques (open_source.selection), affichés en premier
sous un sommaire d'ancres ; grille 2 colonnes dès 1024 px. Le test contrôle
la sélection (champs, https, liste noire) et les doublons d'URL.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136n5xMLHuaBDLcPJYCDjRW
2026-10-08 17:07:37 +02:00

166 lines
9.2 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 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`)