revise September 2018 environment articles
Build and deploy / deploy (push) Successful in 22s

This commit is contained in:
2026-07-31 09:52:09 +03:00
parent 7c53ffe6bc
commit 2c7f9d37d7
9 changed files with 856 additions and 2 deletions
+141
View File
@@ -0,0 +1,141 @@
import { execFile } from 'node:child_process';
import { access, readFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const execFileAsync = promisify(execFile);
const webRoot = join(fileURLToPath(new URL('..', import.meta.url)));
const scriptArgument = process.argv[2];
if (!scriptArgument) {
throw new Error('Usage: node scripts/audit-editorial-draft.mjs <revision-script.mjs>');
}
const scriptPath = isAbsolute(scriptArgument)
? scriptArgument
: resolve(process.cwd(), scriptArgument);
const archive = JSON.parse(await readFile(join(webRoot, 'data', 'articles.json'), 'utf8'));
const archiveBySlug = new Map(archive.map((article) => [article.slug, article]));
const genericPhrases = [
'У этой модели нет магической силы',
'Материалы для проверки',
'Если держать этот порядок, решение остаётся понятным',
'В современном мире',
'очень важно',
'следует отметить',
'просто нужно',
'нужно понимать, что',
];
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>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''),
);
}
const cli = await execFileAsync(process.execPath, [scriptPath, '--print-revisions'], {
cwd: dirname(scriptPath),
encoding: 'utf8',
});
if (cli.stderr.trim()) {
throw new Error('CLI wrote to stderr: ' + cli.stderr.trim());
}
let cliRevisions;
try {
cliRevisions = JSON.parse(cli.stdout);
} catch (error) {
throw new Error('CLI did not print JSON: ' + error.message);
}
const imported = await import(pathToFileURL(scriptPath).href);
const revisions = imported.revisions;
if (!Array.isArray(revisions) || revisions.length !== 3) {
throw new Error('Module must export exactly three revisions');
}
if (JSON.stringify(cliRevisions) !== JSON.stringify(revisions)) {
throw new Error('CLI revisions do not match the import-safe module export');
}
const seenSlugs = new Set();
let failed = false;
for (const revision of revisions) {
const issues = [];
const content = revision.contentHtml || '';
const body = bodyText(content);
const figures = [...content.matchAll(/<figure>([\s\S]*?)<\/figure>/g)].map((match) => match[1]);
const imageSources = [...content.matchAll(/<img[^>]+src="([^"]+)"/g)].map((match) => match[1]);
const proseWithoutCode = content
.replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '')
.replace(/<code>[\s\S]*?<\/code>/g, '');
if (!revision.slug || !archiveBySlug.has(revision.slug)) issues.push('slug отсутствует в базовом архиве');
if (seenSlugs.has(revision.slug)) issues.push('slug повторяется внутри пакета');
seenSlugs.add(revision.slug);
if (Object.hasOwn(revision, 'date') || Object.hasOwn(revision, 'author')) {
issues.push('ревизия не должна менять дату или автора');
}
if (body.length < 5000 || body.length > 15000) issues.push('основной текст вне 5 000–15 000 знаков');
if (revision.readingMinutes < 8) issues.push('меньше 8 минут чтения');
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, /<ol>/g) < 1) issues.push('нет последовательности действий');
if (count(content, /<a href="https?:\/\//g) < 2) issues.push('меньше двух внешних источников');
if (!content.includes('<h2>Проверяемые источники</h2>')) issues.push('нет точного раздела источников');
if (!/(проблем|ошиб|симптом|сбой|задач)/i.test(body.slice(0, 800))) {
issues.push('проблема не названа в начале текста');
}
if (proseWithoutCode.includes('undefined') || proseWithoutCode.includes('[object Object]')) {
issues.push('в тексте есть след генерации');
}
for (const phrase of genericPhrases) {
if (content.includes(phrase)) issues.push('шаблонный оборот: «' + phrase + '»');
}
for (const figure of figures) {
const image = figure.match(/<img\b[^>]*>/);
const alt = image?.[0].match(/\balt="([^"]*)"/);
if (!image || !alt || alt[1].trim().length < 12) {
issues.push('у рисунка нет содержательного alt-текста');
break;
}
}
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 ' + revision.slug + ': ' + issues.join('; '));
} else {
console.log('PASS ' + revision.slug + ': ' + body.length + ' body chars');
}
}
if (failed) process.exitCode = 1;