CI / build-and-test (push) Successful in 14m43s
7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
104 lines
4.2 KiB
JavaScript
104 lines
4.2 KiB
JavaScript
/**
|
|
* 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 = '<!-- GENERATED:provider-matrix (npm run doc:providers) -->';
|
|
const END = '<!-- /GENERATED:provider-matrix -->';
|
|
|
|
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)}`);
|