test(outils): garde-fou des données outils.json (B6-M1)

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136n5xMLHuaBDLcPJYCDjRW
This commit is contained in:
Jules Neny
2026-10-08 16:38:29 +02:00
co-authored by Claude Sonnet
parent 4f0f0d95c9
commit c6123f1f59
+125
View File
@@ -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`)