import { access, readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { editorialRevisions } from '../data/editorial-revisions.mjs'; import { cleanReaderHtml } from '../lib/editorial-content.mjs'; const webRoot = join(fileURLToPath(new URL('..', import.meta.url))); const articlesPath = join(webRoot, 'data', 'articles.json'); const archivedArticles = JSON.parse(await readFile(articlesPath, 'utf8')); const revisionBySlug = new Map( editorialRevisions.map((revision) => [revision.slug, revision]), ); const archive = archivedArticles.map((article) => ({ ...article, ...revisionBySlug.get(article.slug), })); const requestedSlugs = process.argv.slice(2); const slugs = requestedSlugs.includes('--all-articles') ? archive.map((article) => article.slug) : requestedSlugs.includes('--all-editorial') ? archive.filter((article) => article.slug.startsWith('editorial-')).map((article) => article.slug) : requestedSlugs; if (slugs.length === 0) { throw new Error('Usage: node scripts/audit-quality-batch.mjs [...slug] | --all-editorial | --all-articles'); } const genericPhrases = [ 'У этой модели нет магической силы', 'Материалы для проверки', 'Если держать этот порядок, решение остаётся понятным', 'В современном мире', 'очень важно', 'следует отметить', 'просто нужно', 'нужно понимать, что', ]; const editorialMetaPatterns = [ /synthetic-plan-hand-off/i, /productionEffect/i, /future-only/i, /plan\/scenario/i, /source cutoff/i, /editorial date/i, /planDate/i, /not-collected/i, /not-attempted/i, /future owner/i, /author trajectory/i, /развитие автора/i, ]; const MIN_BODY_CHARS = 5000; const MAX_BODY_CHARS = 15000; 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(); } function bodyText(content) { return plainText( content.replace(/

Проверяемые источники<\/h2>[\s\S]*?(?=

|$)/, ''), ); } 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 = cleanReaderHtml(article.contentHtml); const body = bodyText(content); const imageSources = [...content.matchAll(/]+src="([^"]+)"/g)].map((match) => match[1]); const figures = [...content.matchAll(/
([\s\S]*?)<\/figure>/g)].map((match) => match[1]); if (article.readingMinutes < 8) issues.push('указано меньше 8 минут чтения'); if (body.length < MIN_BODY_CHARS) { issues.push('меньше ' + MIN_BODY_CHARS + ' знаков основного текста'); } if (body.length > MAX_BODY_CHARS) { issues.push('больше ' + MAX_BODY_CHARS + ' знаков основного текста'); } 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, /
    /g) < 1) issues.push('нет последовательности проверки или действий'); if (count(content, / value.startsWith('/'))) { try { await access(join(webRoot, 'public', source)); } catch { issues.push('не найден локальный visual asset: ' + source); } } if (issues.length > 0) { failed = true; console.error('FAIL ' + slug + ': ' + issues.join('; ')); } else { console.log( 'PASS ' + slug + ': ' + body.length + ' body chars, ' + count(content, /
    /g) + ' figure, ' + count(content, /
/g) + ' table, ' + count(content, /
/g) + ' code example',
    );
  }
}

if (failed) process.exitCode = 1;