import { access, readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; const webRoot = join(fileURLToPath(new URL('..', import.meta.url))); const articlesPath = join(webRoot, 'data', 'articles.json'); const slugs = process.argv.slice(2); if (slugs.length === 0) { throw new Error('Usage: node scripts/audit-quality-batch.mjs [...slug]'); } const archive = JSON.parse(await readFile(articlesPath, 'utf8')); const genericPhrases = [ 'У этой модели нет магической силы', 'Материалы для проверки', 'Если держать этот порядок, решение остаётся понятным', ]; let failed = false; function count(content, expression) { return (content.match(expression) || []).length; } function plainText(content) { return content .replace(/<[^>]+>/g, ' ') .replace(/&(?:quot|amp|lt|gt|#039);/g, ' ') .replace(/\s+/g, ' ') .trim(); } for (const slug of slugs) { const article = archive.find((candidate) => candidate.slug === slug); const issues = []; if (!article) { console.error('FAIL ' + slug + ': статья не найдена'); failed = true; continue; } const content = article.contentHtml; const text = plainText(content); const imageSources = [...content.matchAll(/]+src="([^"]+)"/g)].map((match) => match[1]); if (article.readingMinutes < 8) issues.push('указано меньше 8 минут чтения'); if (text.length < 4800) issues.push('меньше 4800 символов осмысленного текста'); if (count(content, /

/g) < 5) issues.push('меньше пяти смысловых разделов'); if (count(content, /
/g) < 1 || imageSources.length < 1) issues.push('нет визуального объяснения'); if (count(content, /
/g) < 1) issues.push('у иллюстрации нет подписи'); if (count(content, //g) < 1 || count(content, //g) < 1) issues.push('нет доступной таблицы'); if (count(content, /
/g) < 1) issues.push('нет воспроизводимого примера');
  if (count(content, /