Files
progcode/web/scripts/audit-quality-batch.mjs
T
huncode 44c99a2640
Build and deploy / deploy (push) Successful in 21s
upgrade January 2018 Bitrix articles
2026-07-31 01:44:45 +03:00

85 lines
3.4 KiB
JavaScript

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 <article-slug> [...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(/<img[^>]+src="([^"]+)"/g)].map((match) => match[1]);
if (article.readingMinutes < 8) issues.push('указано меньше 8 минут чтения');
if (text.length < 4800) issues.push('меньше 4800 символов осмысленного текста');
if (count(content, /<h2>/g) < 5) issues.push('меньше пяти смысловых разделов');
if (count(content, /<figure>/g) < 1 || imageSources.length < 1) issues.push('нет визуального объяснения');
if (count(content, /<figcaption>/g) < 1) issues.push('у иллюстрации нет подписи');
if (count(content, /<table>/g) < 1 || count(content, /<thead>/g) < 1) issues.push('нет доступной таблицы');
if (count(content, /<pre><code>/g) < 1) issues.push('нет воспроизводимого примера');
if (count(content, /<a href="https?:\/\//g) < 2) issues.push('меньше двух внешних источников');
if (!content.includes('<h2>Проверяемые источники</h2>')) issues.push('нет отдельного раздела с источниками');
if (content.includes('undefined') || content.includes('[object Object]')) issues.push('в тексте есть след генерации');
for (const phrase of genericPhrases) {
if (content.includes(phrase)) issues.push('обнаружен шаблонный оборот: «' + phrase + '»');
}
for (const source of imageSources.filter((value) => 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
+ ': ' + text.length + ' chars, '
+ count(content, /<figure>/g) + ' figure, '
+ count(content, /<table>/g) + ' table, '
+ count(content, /<pre><code>/g) + ' code example',
);
}
}
if (failed) process.exitCode = 1;