/** * Phase 8.5 — documentation GÉNÉRÉE depuis les fixtures gelées. * * Produit la matrice « champ ↔ provider » à partir de ce que les adaptateurs * émettent RÉELLEMENT, et l'injecte entre deux marqueurs dans * `docs/ingestion-catalogue-video-par-fournisseur.md`. * * Raison d'être : une matrice écrite à la main devient fausse au premier * adaptateur modifié, et personne ne le remarque — c'est le pire sort pour une * doc de référence. Générée, elle ne peut qu'être à jour ou absente. * * La section générée est donc explicitement marquée comme telle, et `npm run * test:shapes` échoue si elle n'est plus synchronisée (dérive = doc fausse). */ import fs from 'node:fs'; import path from 'node:path'; const ROOT = path.resolve(import.meta.dirname, '../..'); const FIXTURES = path.join(ROOT, 'server/tests/fixtures/provider-suggestions.json'); const DOC = path.join(ROOT, 'docs/ingestion-catalogue-video-par-fournisseur.md'); const BEGIN = ''; const END = ''; const data = JSON.parse(fs.readFileSync(FIXTURES, 'utf8')); const ids = Object.keys(data.providers); const labels = { yt: 'YouTube', dm: 'Dailymotion', tw: 'Twitch', pt: 'PeerTube', od: 'Odysee', ru: 'Rumble' }; // Champs d'intérêt métier, dans un ordre de lecture stable (le tri // alphabétique de `Object.keys` n'est pas l'ordre du contrat). const FIELDS = [ ['duration', 'durée', 'secondes'], ['views', 'vues', 'nombre'], ['likes', 'likes', 'nombre'], ['publishedAt', 'publication', 'date ISO'], ['thumbnail', 'vignette', 'URL'], ['uploaderName', 'chaîne', 'texte'], ['channelRef', 'identité chaîne', 'scheme + value'], ['type', 'type', 'video / live / short'], ['kind', 'kind', 'vod / live / clip / channel'], ['isLive', 'direct', 'booléen'], ['width', 'largeur', 'px'], ['height', 'hauteur', 'px'], ['language', 'langue', 'code'], ]; const mark = (present) => (present ? 'x' : '·'); const lines = []; lines.push(BEGIN); lines.push(''); lines.push(`> Section **générée** par \`npm run doc:providers\` depuis \`server/tests/fixtures/provider-suggestions.json\``); lines.push(`> (gel du ${data._recordedAt.slice(0, 10)}, requête \`${data._query}\`). Ne pas éditer à la main.`); lines.push(''); lines.push(`Legende : \`x\` = émis dans le gel, \`·\` = absent (donnée inconnue, donc \`undefined\` côté front).`); lines.push(''); const head = ['Champ', 'Type', ...ids.map((id) => labels[id] || id)]; lines.push(`| ${head.join(' | ')} |`); lines.push(`|${head.map(() => '---').join('|')}|`); for (const [field, label, type] of FIELDS) { const cells = ids.map((id) => { const items = data.providers[id]?.items || []; // Sur un provider sans fixture, on ne peut rien affirmer : ni x ni ·. if (items.length === 0) return '?'; return mark(items.some((it) => it[field] !== undefined)); }); lines.push(`| \`${field}\` (${label}) | ${type} | ${cells.join(' | ')} |`); } lines.push(''); lines.push('Couverture du gel :'); lines.push(''); for (const id of ids) { const entry = data.providers[id]; const n = entry.items.length; lines.push(n ? `- **${labels[id] || id}** : ${n} item(s) vérifié(s).` : `- **${labels[id] || id}** : *non couvert* — ${entry.note || 'aucune fixture'}.`); } lines.push(''); lines.push('(`?` = provider sans fixture au gel : la matrice ne prétend rien sur lui.)'); lines.push(''); lines.push(END); const block = lines.join('\n'); const doc = fs.readFileSync(DOC, 'utf8'); if (doc.includes(BEGIN)) { const from = doc.indexOf(BEGIN); const to = doc.indexOf(END) + END.length; fs.writeFileSync(DOC, doc.slice(0, from) + block + doc.slice(to)); console.log('matrice régénérée (remplacement)'); } else { // Ancre : juste après le titre de la section correspondente. const anchor = '## 4. Catalogue'; const at = doc.indexOf(anchor); if (at < 0) throw new Error(`ancre « ${anchor} » introuvable dans ${path.basename(DOC)}`); const insertAt = doc.indexOf('\n', at) + 1; fs.writeFileSync(DOC, `${doc.slice(0, insertAt)}\n${block}\n${doc.slice(insertAt)}`); console.log('matrice générée (insertion)'); } console.log(`→ ${path.relative(process.cwd(), DOC)}`);