Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136n5xMLHuaBDLcPJYCDjRW
126 lines
6.8 KiB
JavaScript
126 lines
6.8 KiB
JavaScript
#!/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`)
|