307 lines
73 KiB
JavaScript
307 lines
73 KiB
JavaScript
import archive from '../data/articles.json' with { type: 'json' };
|
||
const archiveBySlug = new Map(archive.map((article) => [article.slug, article]));
|
||
|
||
function p(text) {
|
||
return '<p>' + text + '</p>';
|
||
}
|
||
|
||
function h2(text) {
|
||
return '<h2>' + text + '</h2>';
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return value
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>');
|
||
}
|
||
|
||
function codeBlock(lines) {
|
||
return '<pre><code>' + escapeHtml(lines.join('\n')) + '</code></pre>';
|
||
}
|
||
|
||
function table(headers, rows) {
|
||
const head = '<thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead>';
|
||
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody>';
|
||
return '<table><caption>' + headers[0] + ': рабочая матрица проверки</caption>' + head + body + '</table>';
|
||
}
|
||
|
||
function orderedList(items) {
|
||
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||
}
|
||
|
||
function figure(src, alt, caption) {
|
||
return '<figure><img src="' + src + '" alt="' + alt + '" /><figcaption>' + caption + '</figcaption></figure>';
|
||
}
|
||
|
||
function sources(items) {
|
||
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.label + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
||
}
|
||
|
||
function plainText(content) {
|
||
return content
|
||
.replace(/<[^>]+>/g, ' ')
|
||
.replace(/&(?:quot|amp|lt|gt|#039);/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function legacyBody(article, limit = 6600, omitPatterns = []) {
|
||
const withoutSources = (article.contentHtml || '')
|
||
.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*$/i, '')
|
||
.replace(/<figure>[\s\S]*?<\/figure>/gi, '')
|
||
.replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, '$1')
|
||
.replace(/<\/?div[^>]*>/gi, '');
|
||
const blocks = withoutSources.match(/<(?:p|h2|h3|pre|ul|ol)\b[\s\S]*?<\/(?:p|h2|h3|pre|ul|ol)>/gi) || [];
|
||
const result = [];
|
||
let length = 0;
|
||
for (const block of blocks) {
|
||
if (omitPatterns.some((pattern) => pattern.test(plainText(block)))) continue;
|
||
const blockLength = plainText(block).length;
|
||
if (result.length > 0 && length + blockLength > limit) break;
|
||
result.push(block);
|
||
length += blockLength;
|
||
}
|
||
return result.join('\n');
|
||
}
|
||
|
||
const cases = {
|
||
'использование-jquery-в-webpack': {
|
||
title: 'JavaScript. jQuery в Webpack: глобальная зависимость без скрытого порядка',
|
||
excerpt: 'Как подключить jQuery к Webpack-проекту, когда часть кода ждёт window.jQuery, а новые модули используют import.',
|
||
cover: '/assets/illustrations/jquery-webpack.svg',
|
||
problem: 'Старая страница видит `$`, а новый модуль получает пустое значение. Цена ошибки — либо дублированная библиотека в каждом бандле, либо плагины, которые работают только из-за случайного порядка подключения.',
|
||
context: 'Оригинальный материал правильно начинает с установки пакета и `ProvidePlugin`. В редактуре важно разделить две задачи: дать старому коду совместимое глобальное имя и оставить импорт явным в новых модулях. Если смешать их в одном правиле сборки, после смены entry-файла ошибка проявится только на части страниц.',
|
||
mechanism: ['Webpack строит граф модулей из import и require. Глобальная переменная не входит в этот граф как обычная зависимость, поэтому старый плагин может работать только при дополнительном правиле ProvidePlugin или явной записи в window.', 'ProvidePlugin подставляет импорт в местах, где встречается идентификатор. Это не делает jQuery глобальной для любого скрипта, загруженного отдельно через HTML. Для такого скрипта нужен один согласованный entry и одна точка экспорта.', 'Если библиотека уже приходит с CDN, её следует объявить external и проверить, что глобальное имя появляется раньше потребителя. Две независимые копии jQuery дают разные объекты и ломают плагины, которые сравнивают `$.fn` или регистрируют обработчики.'],
|
||
code: ['const webpack = require(\'webpack\');', '', 'module.exports = {', ' entry: {', ' legacy: \'./src/legacy-entry.js\',', ' modern: \'./src/modern-entry.js\'', ' },', ' plugins: [', ' new webpack.ProvidePlugin({', ' $: \'jquery\',', ' jQuery: \'jquery\'', ' })', ' ]', '};', '', '// В новом модуле зависимость остаётся видимой.', "import $ from 'jquery';", 'export function mount() {', ' return $.fn && $.fn.jquery;', '}'],
|
||
rows: [['Слой', 'Что проверяем', 'Типичная ошибка'], ['Новый модуль', 'Есть `import $ from \'jquery\'`', 'Зависимость спрятана в window'], ['Старый плагин', 'Потребитель получает тот же объект', 'Созданы две копии jQuery'], ['Entry', 'Библиотека загружена до потребителя', 'Порядок зависит от HTML'], ['CDN', 'external и глобальное имя согласованы', 'Бандл ожидает модуль, а получает URL']],
|
||
steps: ['Определить, какие файлы используют import, а какие обращаются к `$` или `window.jQuery`.', 'Оставить один способ доставки jQuery для каждого entry: пакет или external, но не случайную смесь.', 'Добавить ProvidePlugin только для legacy-кода и проверить итоговый граф сборки.', 'Запустить страницу с реальным старым плагином и убедиться, что объект jQuery один.', 'После миграции каждого потребителя удалить лишнее глобальное правило и зафиксировать это в тесте сборки.'],
|
||
limits: ['ProvidePlugin не исправляет порядок независимых `<script>`-тегов в HTML.', 'Версия jQuery и версия плагина должны быть частью контракта: зелёная сборка не доказывает совместимость поведения.', 'Пример показывает конфигурацию Webpack, а не измерение размера конкретного проекта.'],
|
||
omitPatterns: [/В таком случае только в модуле/i],
|
||
sources: [{ label: 'Webpack: ProvidePlugin', url: 'https://webpack.js.org/plugins/provide-plugin/', note: 'описывает автоматическую подстановку импорта' }, { label: 'Webpack: externals', url: 'https://webpack.js.org/configuration/externals/', note: 'фиксирует правила для внешних зависимостей' }, { label: 'jQuery API', url: 'https://api.jquery.com/', note: 'проверяет публичный объект и методы библиотеки' }],
|
||
},
|
||
'bitrix-api-add-foto-editor': {
|
||
title: 'Bitrix API. Встроенный редактор изображений: UI не заменяет серверный контракт',
|
||
excerpt: 'Как подключить FileInput в Bitrix и проверить, что выбранный файл действительно привязан к сущности после сохранения.',
|
||
cover: '/assets/illustrations/bitrix-photo-editor.svg',
|
||
problem: 'Редактор показывает выбранную картинку, но после отправки формы поле остаётся пустым. Цена ошибки — оператор считает файл сохранённым и узнаёт о потере только на опубликованной странице.',
|
||
context: 'В исходной заметке полезен сам вызов `Bitrix\\Main\\UI\\FileInput`. Теперь рядом с ним нужен контроль жизненного цикла: UI создаёт данные для формы, сервер проверяет вход, а сущность хранит итоговую связь. Каждый слой должен иметь собственную проверку.',
|
||
mechanism: ['Компонент FileInput умеет показать диалог и собрать поля формы, но не знает бизнес-правило, к какой записи относится файл. Идентификатор сущности и имя поля должны приходить из серверного контекста, а не из DOM.', 'После отправки сервер проверяет права, тип, размер и число файлов. Одного расширения недостаточно: MIME и содержимое могут расходиться. Для замены картинки нужно отдельно определить, удаляется ли старый файл и можно ли восстановить его при ошибке.', 'Результат сохранения проверяется чтением сущности. Наличие `ID` файла в ответе формы ещё не доказывает, что связь записана в нужное свойство инфоблока.'],
|
||
code: ['<?php', '$field = \\Bitrix\\Main\\UI\\FileInput::createInstance([', ' "name" => "picture",', ' "upload" => true,', ' "allowUpload" => "I",', ' "maxCount" => 1,', ' "delete" => true,', ']);', '', 'echo $field->show($elementId);', '', '// После POST проверяем результат API, а не только состояние виджета.', '$fileId = (int)($request->getPost("picture") ?? 0);', 'if ($fileId <= 0) {', ' throw new RuntimeException("Файл не выбран");', '}'],
|
||
rows: [['Участок', 'Вопрос', 'Проверка'], ['Форма', 'Поля отправились?', 'POST содержит ожидаемое имя и ID'], ['Права', 'Кто меняет файл?', 'Пользователь может редактировать сущность'], ['Файл', 'Что реально загружено?', 'Размер, MIME, расширение и содержимое'], ['Связь', 'Куда записан ID?', 'Повторное чтение свойства элемента']],
|
||
steps: ['Назвать сущность, поле и владельца операции до подключения редактора.', 'Показать FileInput с ограничением количества и разрешённых типов.', 'Проверить POST на сервере и отклонить неожиданные поля или пустой ID.', 'Сохранить связь через API Bitrix и обработать `LAST_ERROR` или исключение.', 'Повторно прочитать свойство элемента и проверить опубликованный сценарий.', 'Для замены файла отдельно зафиксировать поведение старого файла и откат при ошибке.'],
|
||
limits: ['Названия параметров и доступные режимы зависят от версии Bitrix; перед переносом сверяйте API своего проекта.', 'Встроенный редактор не заменяет антивирусную и серверную проверку загрузки.', 'Пример не содержит реального ID или данных пользователя.'],
|
||
omitPatterns: [/^Параметры:/i],
|
||
sources: [{ label: 'Bitrix: FileInput', url: 'https://dev.1c-bitrix.ru/api_help/main/ui/fileinput/index.php', note: 'показывает API компонента загрузки' }, { label: 'Bitrix: CIBlockElement', url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/index.php', note: 'описывает работу с элементом инфоблока' }, { label: 'OWASP: File Upload Cheat Sheet', url: 'https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html', note: 'даёт независимый список серверных проверок' }],
|
||
},
|
||
'о-tilix-и-d-интервью-с-геральдом-нанном': {
|
||
title: 'Tilix и D: что инженер проверяет в инструменте, прежде чем выбрать его',
|
||
excerpt: 'Редакторская версия интервью о Tilix и D: как отделить личный сценарий терминала от проверяемых свойств инструмента и языка.',
|
||
cover: '/assets/illustrations/tilix-cover.svg',
|
||
problem: 'Интервью о любимом инструменте легко превращается в список восторгов. Цена такой подачи — читатель переносит чужой рабочий сценарий на свою среду и получает неудобный интерфейс или неподходящий стек.',
|
||
context: 'Сохраняю в материале разговор с Геральдом Нанном и технические детали Tilix, но выношу из него критерии, которые можно проверить самостоятельно. Интервью — источник опыта автора проекта, а не доказательство, что инструмент лучше любого другого терминала.',
|
||
mechanism: ['Tilix решает конкретную задачу: разделяет терминальные сессии в одном окне и сохраняет рабочий контекст. Ценность появляется там, где человеку действительно нужно видеть несколько процессов рядом; при одной короткой команде тайлинг только добавляет интерфейс.', 'D в таком проекте важен не как рекламная метка, а как набор свойств реализации: нативный бинарник, доступ к GTK через библиотеку и автоматическое управление памятью. Эти свойства нужно сопоставлять с размером приложения, зависимостями платформы и способом доставки.', 'Любой отзыв из интервью следует превратить в проверку: открыть исходный код, собрать минимальную версию, проверить горячие клавиши и замерить время запуска в своей среде. Так личная история становится полезной инженерной гипотезой.'],
|
||
code: ['# Проверка собственного сценария: два процесса и один лог', 'tilix --session ~/.config/tilix/sessions/debug.json', '', '# В проекте D сначала фиксируем версию компилятора.', 'dmd --version', 'dub --version', '', '# Затем собираем без изменения пользовательской системы.', 'dub build --build=debug'],
|
||
rows: [['Критерий', 'Что видно в интервью', 'Что проверить у себя'], ['Рабочее окно', 'Несколько терминалов в одном layout', 'Сколько панелей реально нужно ежедневно'], ['Интеграция', 'GTK и desktop-сценарий', 'Версия GTK и поведение на целевой ОС'], ['Язык', 'D используется для приложения', 'Версия DMD/LDC, время сборки и зависимости'], ['Сопровождение', 'Открытый проект и сообщество', 'Релизы, issue tracker и способ собрать пакет']],
|
||
steps: ['Описать один повторяемый сценарий: какие команды запускаются и что нужно видеть одновременно.', 'Отделить свойства Tilix от свойств конкретной конфигурации автора интервью.', 'Проверить версию приложения, GTK и компилятора в своей системе.', 'Собрать маленький D-проект и измерить путь от исходников до запуска.', 'Сравнить результат с привычным терминалом по времени и количеству ручных действий.', 'Оставить инструмент, если он сокращает конкретную работу; не делать вывод по числу функций.'],
|
||
limits: ['Интервью датировано 2017 годом. Состояние репозитория, пакетов и платформ с тех пор менялось.', 'Опыт одного разработчика не является benchmark и не заменяет тестирование на целевой ОС.', 'Сборка с `dub` и `dmd` требует конкретных версий и системных библиотек.'],
|
||
omitPatterns: [/^Несмотря на то, что Tilix/i],
|
||
sources: [{ label: 'Tilix: исходный код', url: 'https://github.com/gnunn1/tilix', note: 'проверяет актуальную структуру и инструкции сборки' }, { label: 'D language: спецификация', url: 'https://dlang.org/spec/', note: 'фиксирует правила языка, а не личные оценки инструмента' }, { label: 'D: automatic memory management', url: 'https://dlang.org/spec/garbage.html', note: 'помогает проверить тезисы об управлении памятью' }],
|
||
},
|
||
'bitrix-api-функция-для-генерации-кода-элемент': {
|
||
title: 'Bitrix API. Символьный код элемента: транслитерация — только первый шаг',
|
||
excerpt: 'Как получить стабильный CODE из названия в Bitrix и не сломать URL повтором, разделом или массовым обновлением.',
|
||
cover: '/assets/illustrations/bitrix-translit-api.svg',
|
||
problem: 'Транслитерация возвращает красивую строку, но два товара получают одинаковый CODE. Цена ошибки — дубли адресов, неверная карточка и необходимость чинить старые ссылки.',
|
||
context: 'Исходная функция `CUtil::translit` остаётся хорошей точкой входа. Редактура добавляет три обязательных слоя: нормализацию результата, проверку уникальности и правило поведения при смене уже опубликованного кода.',
|
||
mechanism: ['Транслитератор решает только преобразование символов. Он не знает инфоблок, язык витрины и уже занятые адреса. Поэтому CODE нужно считать частью данных элемента, а не производным текстовым украшением.', 'Уникальность проверяется в том же контексте, где код будет использоваться. Для каталога это обычно инфоблок и, возможно, раздел. Если URL глобальный, суффикс нужно добавлять до записи, а не после ошибки маршрута.', 'Смена CODE — изменение публичного адреса. Нужны редирект или таблица старых ссылок, а также проверка всех мест, где код сохранён как внешний идентификатор.'],
|
||
code: ['function makeElementCode(string $name, int $id = 0): string', '{', ' $base = CUtil::translit($name, "ru", [', ' "max_len" => 80,', ' "change_case" => "L",', ' "replace_space" => "-",', ' "replace_other" => "-",', ' "delete_repeat_replace" => true,', ' ]);', ' $base = trim(preg_replace("/-+", "-", $base), "-");', ' $code = $base ?: "item";', ' $suffix = 1;', ' while (codeExists($code, $id)) {', ' $suffix++;', ' $code = $base . "-" . $suffix;', ' }', ' return $code;', '}'],
|
||
rows: [['Шаг', 'Вход', 'Результат'], ['Транслитерация', 'Название на русском', 'Кандидат CODE'], ['Нормализация', 'Пробелы и повторные дефисы', 'Одна каноническая строка'], ['Уникальность', 'Кандидат и ID текущего элемента', 'Свободный код или суффикс'], ['Публикация', 'Старый и новый URL', 'Правило редиректа или ссылки']],
|
||
steps: ['Выбрать область уникальности: весь инфоблок, раздел или отдельная витрина.', 'Сгенерировать кандидат через `CUtil::translit` и нормализовать дефисы.', 'Проверить существующий CODE, исключив текущий ID при редактировании.', 'Сохранить код только после успешной проверки обязательных данных.', 'Для опубликованного элемента сохранить старый URL и проверить переход.', 'Добавить тесты для пустого имени, Unicode, повторов и одинаковых названий.'],
|
||
limits: ['Правило транслитерации и параметры `CUtil` зависят от версии Bitrix.', 'Уникальность в базе должна быть защищена от гонки при параллельном создании.', 'Суффикс исправляет конфликт, но не решает вопрос понятного URL для бизнеса.'],
|
||
sources: [{ label: 'Bitrix: CUtil::translit', url: 'https://dev.1c-bitrix.ru/api_help/main/functions/cutil/translit.php', note: 'описывает параметры транслитерации' }, { label: 'Bitrix: CIBlockElement', url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/index.php', note: 'показывает контекст записи элемента' }, { label: 'Bitrix: URL rewrite', url: 'https://dev.1c-bitrix.ru/api_help/main/general/urlrewrite.php', note: 'помогает проверить последствия смены адреса' }],
|
||
},
|
||
'bitrix-api-создание-добавление-торгового-пре': {
|
||
title: 'Bitrix API. Торговое предложение: создать запись и довести её до каталога',
|
||
excerpt: 'Почему одного CIBlockElement::Add недостаточно для торгового предложения и как проверить связь товара, свойств, цены и остатка.',
|
||
cover: '/assets/illustrations/bitrix-offer-api.svg',
|
||
problem: 'Предложение создаётся без ошибки, но не появляется в карточке товара. Цена ошибки — администратор видит ID записи, а покупатель не видит вариант, цену или остаток.',
|
||
context: 'В старом рецепте важен вызов API создания. Для читателя нужно явно разделить элемент инфоблока и товарную модель: предложение — это запись, связанная с товаром, а цена и остаток живут в дополнительных сущностях каталога.',
|
||
mechanism: ['`CIBlockElement::Add` возвращает ID элемента, но не гарантирует, что у него есть обязательные свойства связи. Сначала сохраняют минимальный набор полей и проверяют ошибку, затем создают или обновляют товарную часть.', 'Связь с родительским товаром должна быть однозначной. Если свойство XML_ID, SKU или ID заполнено неверно, каталог может показать пустой результат, хотя запись физически есть.', 'После записи нужна выборка тем же способом, которым читает каталог. Это ловит ошибки активности, дат, прав, раздела и кеша без догадки о том, какая кнопка «обновить» поможет.'],
|
||
code: ['<?php', '$element = new CIBlockElement();', '$offerId = $element->Add([', ' "IBLOCK_ID" => $offerIblockId,', ' "NAME" => "Кофе 250 г",', ' "ACTIVE" => "Y",', ' "PROPERTY_VALUES" => [', ' "CML2_LINK" => $productId,', ' "ARTNUMBER" => "COFFEE-250",', ' ],', ']);', '', 'if (!$offerId) {', ' throw new RuntimeException($element->LAST_ERROR);', '}', '', '// Затем отдельно проверяем цену, остаток и связь с productId.'],
|
||
rows: [['Слой', 'Что сохраняется', 'Чем подтверждается'], ['Инфоблок', 'ID, имя, активность, свойства', 'Повторная выборка элемента'], ['Связь', 'ID родительского товара', 'Свойство связи совпадает с товаром'], ['Цена', 'Тип цены и значение', 'Запрос каталога возвращает цену'], ['Остаток', 'Количество и доступность', 'Карточка показывает ожидаемый статус']],
|
||
steps: ['Назвать ID инфоблока предложения и ID родительского товара.', 'Сохранить элемент через API и сразу обработать `LAST_ERROR`.', 'Проверить обязательное свойство связи и уникальный артикул.', 'Создать или обновить товарные параметры отдельным шагом.', 'Прочитать предложение через тот же компонент или запрос, что использует витрина.', 'Только после проверки данных обновить кеш и индекс по правилам проекта.'],
|
||
limits: ['API каталога менялся между версиями Bitrix; исторический код нельзя переносить без сверки версии.', 'ID элемента не подтверждает наличие цены, остатка или видимость в компоненте.', 'Массовую загрузку нужно защищать от повторного запуска и гонки уникальности.'],
|
||
sources: [{ label: 'Bitrix: CIBlockElement::Add', url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/add.php', note: 'описывает создание элемента и обработку ошибки' }, { label: 'Bitrix: каталог', url: 'https://dev.1c-bitrix.ru/api_help/catalog/', note: 'разделяет элемент, товар и цены' }, { label: 'Bitrix: торговые предложения', url: 'https://dev.1c-bitrix.ru/api_help/catalog/classes/ccatalogsku/index.php', note: 'помогает проверить связь SKU с товаром' }],
|
||
},
|
||
'firefox-увеличение-ожидания-загрузки-timeout-стр': {
|
||
title: 'Firefox. Таймаут страницы: увеличиваем ожидание только после проверки причины',
|
||
excerpt: 'Как отличить медленный ответ сервера от сетевого сбоя и изменить параметр Firefox так, чтобы не скрыть проблему.',
|
||
cover: '/assets/illustrations/firefox-timeout.svg',
|
||
problem: 'Firefox прекращает загрузку долгого запроса, и первая реакция — увеличить timeout. Цена ошибки — браузер дольше держит пользователя на сломанном соединении, а серверная причина остаётся без диагностики.',
|
||
context: 'В исходной инструкции полезно само указание на `about:config`. Редактура добавляет порядок: сначала DevTools и другой клиент, затем проверка прокси и сервера, и только потом временная настройка с понятным сроком возврата.',
|
||
mechanism: ['Таймаут браузера — один из нескольких пределов. Запрос могут завершить сервер, reverse proxy, балансировщик или сеть. Если изменить только Firefox, он не продлит соединение, которое уже закрывает другой слой.', 'В Network-панели нужно посмотреть, на каком этапе проходит время: DNS, соединение, ожидание первого байта или загрузка тела. Длинный TTFB указывает на серверную работу; обрыв соединения — на другой класс причины.', 'Параметр в `about:config` действует локально и может меняться между версиями. Это диагностическая мера, а не настройка приложения для всех пользователей.'],
|
||
code: ['# 1. Проверить тот же URL без браузерного UI.', 'curl -v --max-time 60 https://example.test/report', '', '# 2. В Firefox открыть about:config и найти параметр', '# network.http.response.timeout', '# Сохранить старое значение и менять его только в профиле диагностики.', '', '# 3. После исправления вернуть параметр и повторить Network trace.'],
|
||
rows: [['Наблюдение', 'Где искать причину', 'Следующий шаг'], ['Нет ответа до первого байта', 'Приложение, БД, внешний API', 'Снять серверный trace и время этапов'], ['Соединение закрыто прокси', 'Reverse proxy или балансировщик', 'Сверить таймауты всех уровней'], ['Только Firefox падает', 'Профиль, расширение, локальная настройка', 'Повторить в чистом профиле'], ['curl и браузер одинаково медленны', 'Сервер или сеть', 'Не менять браузерный параметр первым']],
|
||
steps: ['Записать URL, время начала запроса и наблюдаемый этап в DevTools.', 'Повторить запрос через `curl` с ограниченным таймаутом.', 'Сравнить таймауты браузера, прокси, сервера и внешних зависимостей.', 'Изменить параметр только в отдельном Firefox-профиле и сохранить старое значение.', 'После исправления вернуть параметр и проверить, что причина устранена, а не замаскирована.'],
|
||
limits: ['Названия и смысл внутренних параметров Firefox могут меняться; сверяйте версию браузера.', 'Увеличенный timeout не делает операцию быстрее и не заменяет отмену запроса.', 'Не просите пользователя менять `about:config` как постоянное решение серверной проблемы.'],
|
||
sources: [{ label: 'Mozilla Support: Firefox не загружает сайты', url: 'https://support.mozilla.org/en-US/kb/firefox-cant-load-websites-other-browsers-can', note: 'даёт порядок проверки соединения и расширений' }, { label: 'MDN: Network requests', url: 'https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor', note: 'объясняет поля сетевого запроса в DevTools' }, { label: 'Firefox source code', url: 'https://searchfox.org/mozilla-central/search?q=network.http.response.timeout', note: 'помогает сверить исторический параметр с версией' }],
|
||
},
|
||
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': {
|
||
title: 'PHP. unable to get local issuer certificate: восстановить цепочку доверия, не отключая TLS',
|
||
excerpt: 'Как диагностировать ошибку CA bundle в cURL и PHP и настроить доверенный корень без `verify_peer=false`.',
|
||
cover: '/assets/illustrations/php-ssl-cacert.svg',
|
||
problem: 'PHP-клиент не принимает сертификат, хотя сайт открывается в браузере. Цена ошибки — команда отключает проверку TLS и превращает временную проблему с CA bundle в постоянную уязвимость.',
|
||
context: 'Старый рецепт часто сводится к скачиванию `cacert.pem`. В качественной версии важнее понять цепочку: какой клиент выполняет запрос, где он ищет доверенные корни, какой сертификат прислал сервер и совпадает ли окружение CLI с окружением FPM.',
|
||
mechanism: ['Клиент проверяет не только имя хоста. Он строит цепочку от сертификата сервера до доверенного корня и сверяет срок, имя и подпись. Если промежуточный сертификат не пришёл или локальное хранилище устарело, появляется `unable to get local issuer certificate`.', 'Браузер и PHP могут использовать разные хранилища. Поэтому «в браузере зелёный замок» не доказывает, что CLI-cURL видит тот же CA bundle.', 'Исправление должно менять путь к актуальному bundle или хранилище ОС. Отключение `CURLOPT_SSL_VERIFYPEER` и `CURLOPT_SSL_VERIFYHOST` убирает доказательство подлинности и не является диагностикой.'],
|
||
code: ['<?php', '$ch = curl_init("https://example.test/health");', 'curl_setopt_array($ch, [', ' CURLOPT_RETURNTRANSFER => true,', ' CURLOPT_CAINFO => __DIR__ . "/certs/cacert.pem",', ' CURLOPT_SSL_VERIFYPEER => true,', ' CURLOPT_SSL_VERIFYHOST => 2,', ']);', '$body = curl_exec($ch);', 'if ($body === false) {', ' throw new RuntimeException(curl_error($ch));', '}', 'curl_close($ch);'],
|
||
rows: [['Проверка', 'Что смотрим', 'Почему это важно'], ['Сервер', 'Цепочка сертификатов и имя', 'Клиент должен получить промежуточный сертификат'], ['PHP', 'Версия и cURL/OpenSSL', 'CLI и FPM могут иметь разные настройки'], ['CA bundle', 'Путь и дата обновления', 'Старый корень даёт ложную ошибку'], ['TLS-режим', 'verify peer/host включены', 'Отключение проверки нельзя считать исправлением']],
|
||
steps: ['Получить ошибку с включённым TLS verify и сохранить точный текст.', 'Проверить цепочку сервера через `openssl s_client` или доверенный TLS checker.', 'Узнать CA bundle для CLI и FPM, не смешивая окружения.', 'Обновить доверенное хранилище из официального источника и задать путь через конфигурацию.', 'Повторить запрос с `CURLOPT_SSL_VERIFYPEER=true` и `CURLOPT_SSL_VERIFYHOST=2`.', 'Добавить проверку в deployment или health-check, чтобы bundle не устарел незаметно.'],
|
||
limits: ['Не скачивайте CA bundle по тому же неподтверждённому соединению, которое пытаетесь проверить.', 'Сертификат с самоподписанным корнем требует отдельного доверенного процесса, а не отключения проверки.', 'Пути `php.ini` и расширения различаются между CLI, FPM и контейнером.'],
|
||
sources: [{ label: 'PHP: cURL configuration', url: 'https://www.php.net/manual/en/curl.configuration.php', note: 'описывает `curl.cainfo` и настройки расширения' }, { label: 'curl: SSL certificate verification', url: 'https://curl.se/docs/sslcerts.html', note: 'объясняет CA store и цепочку доверия' }, { label: 'OpenSSL: verify', url: 'https://docs.openssl.org/3.0/man1/openssl-verification-options/', note: 'помогает проверить сертификат вне PHP' }],
|
||
},
|
||
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': {
|
||
title: 'DConf 2017. Сборщик мусора D: наблюдаем паузу, а не обвиняем GC вслепую',
|
||
excerpt: 'Редакторская версия заметки о GC в D: как отличить давление на память, паузу сборки и лишние аллокации.',
|
||
cover: '/assets/illustrations/dconf-gc-cover.svg',
|
||
problem: 'Программа на D начинает отвечать рывками, и вся причина объявляется «медленным сборщиком». Цена ошибки — отключить полезную автоматическую память и получить утечки или ещё более дорогие ручные освобождения.',
|
||
context: 'В исходной заметке сохраняю материалы DConf и объяснение устройства GC. Сокращаю повторения и добавляю практическую рамку: сначала измеряем аллокации и размер heap, затем смотрим профиль паузы, и только после этого выбираем настройку.',
|
||
mechanism: ['GC в D освобождает недостижимые объекты, но стоимость возникает не только в момент полной коллекции. Лишние временные объекты увеличивают объём работы, а долгоживущие ссылки удерживают память и меняют частоту циклов.', 'Пауза зависит от реализации runtime, размера набора объектов и режима приложения. Один замер в debug-сборке не говорит, что такой же результат будет у release-бинарника с другой нагрузкой.', 'Ручной `GC.collect` может быть полезен на границе batch-операции, но вызов в каждом цикле обычно только переносит работу в горячее место. Сначала нужно увидеть, где создаётся давление.'],
|
||
code: ['import core.memory : GC;', 'import std.stdio : writeln;', '', 'void processBatch(const int[] values) {', ' auto before = GC.stats();', ' foreach (value; values) {', ' // Не создаём временную строку на каждой итерации без причины.', ' auto squared = value * value;', ' writeln(squared);', ' }', ' auto after = GC.stats();', ' writeln("allocated: ", after.usedSize - before.usedSize);', '}', '', '// GC.collect() — отдельный эксперимент на границе batch,', '// а не универсальный вызов в каждой функции.'],
|
||
rows: [['Сигнал', 'Что измерить', 'Не делать первым'], ['Редкие длинные паузы', 'Длительность и частоту GC', 'Сразу отключать GC'], ['Рост heap', 'Кто удерживает долгоживущие ссылки', 'Увеличивать лимит без профиля'], ['Много мелких объектов', 'Аллокации в горячем цикле', 'Оптимизировать только сборщик'], ['Batch-задача', 'Пауза на границе операции', 'Вызывать collect на каждой итерации']],
|
||
steps: ['Собрать release-профиль на повторяемом наборе данных.', 'Разделить время приложения и время GC, а также размер занятой памяти.', 'Найти горячие места, где создаются временные объекты.', 'Повторить замер после уменьшения аллокаций, не меняя сразу режим GC.', 'Проверить `GC.collect` только на явной границе batch и сравнить паузу.', 'Оставить настройку рядом с числом измерения и условием, при котором она нужна.'],
|
||
limits: ['Детали реализации и профили GC зависят от версии runtime D.', 'Слайды конференции объясняют модель, но не заменяют профиль вашей программы.', 'В примере нет реального production-нагрузочного замера.'],
|
||
sources: [{ label: 'D language: garbage collection', url: 'https://dlang.org/spec/garbage.html', note: 'описывает модель автоматической памяти D' }, { label: 'D: core.memory', url: 'https://dlang.org/library/core/memory.html', note: 'показывает API runtime для наблюдения и управления GC' }, { label: 'DConf', url: 'https://dconf.org/2017/', note: 'сохраняет исторический контекст конференции 2017 года' }],
|
||
},
|
||
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': {
|
||
title: 'D и регулярные выражения: повторяем preg_match_all, не теряя смысл совпадений',
|
||
excerpt: 'Как построить аналог поиска всех совпадений в D и проверить различия между PHP-массивом и диапазоном matchAll.',
|
||
cover: '/assets/illustrations/preg-match-all-d.svg',
|
||
problem: 'Перенос регулярного выражения из PHP в D компилируется, но возвращает другой набор групп. Цена ошибки — неверно разобранный текст, который выглядит корректным до первого вложенного или пустого совпадения.',
|
||
context: 'Исходная статья полезна как портирование знакомой функции. Здесь оно превращается в контракт: что считается совпадением, какие группы нужны, как обрабатывается отсутствие результата и где заканчивается ответственность regex.',
|
||
mechanism: ['`preg_match_all` возвращает структуру, зависящую от режима и групп. В D `std.regex` даёт диапазон совпадений, который удобно обходить, но порядок доступа к группам нужно зафиксировать отдельно.', 'Регулярное выражение не валидирует весь бизнес-формат автоматически. Если нужен полный разбор строки, проверяем якоря `^` и `$`; если нужно найти части внутри текста, не добавляем их случайно.', 'Пустое совпадение опасно для цикла: выражение может не продвинуть позицию так, как ожидает вызывающий код. Тест на пустую строку и соседние совпадения обязателен.'],
|
||
code: ['import std.regex : regex, matchAll;', 'import std.stdio : writeln;', '', 'auto pattern = regex(`(?P<key>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>[^;]+)`);', 'auto input = "mode=fast; retries=3";', '', 'foreach (m; matchAll(input, pattern)) {', ' writeln(m["key"], " => ", m["value"]);', '}', '', '// Ожидаемый вывод:', '// mode => fast', '// retries => 3'],
|
||
rows: [['Вопрос', 'PHP', 'D'], ['Все совпадения', '`preg_match_all`', '`matchAll` как диапазон'], ['Группа', 'Индекс или имя', 'Индекс или именованная группа'], ['Нет результата', 'Пустой массив/false', 'Пустой диапазон'], ['Проверка формата', 'Отдельная логика', 'Отдельная логика']],
|
||
steps: ['Записать пример входа и ожидаемые совпадения до переноса.', 'Перенести шаблон и отдельно проверить экранирование строкового литерала D.', 'Выбрать доступ к группам по имени или индексу и закрепить его тестом.', 'Проверить пустой вход, две соседние записи и значение с разделителем.', 'Сравнить количество и содержимое совпадений с эталоном PHP.', 'Если формат стал сложнее regex, передать результат полноценному парсеру.'],
|
||
limits: ['Совместимость синтаксиса PCRE и regex D не абсолютна; сверяйте документацию конкретной версии.', 'Регулярное выражение не заменяет проверку размера, кодировки и семантики значения.', 'Пример разбирает учебную строку и не читает пользовательский файл.'],
|
||
omitPatterns: [
|
||
/^Описание функции:/i,
|
||
/preg_match_all\s*\(/i,
|
||
/^class PReg/i,
|
||
/^typePatternOrder/i,
|
||
/^auto stdMatches/i,
|
||
/^while \(/i,
|
||
/^assert\(/i,
|
||
/^module preg/i,
|
||
/^import std\.string/i,
|
||
/^match\.position/i,
|
||
/^alias type/i,
|
||
],
|
||
sources: [{ label: 'D: std.regex', url: 'https://dlang.org/phobos/std_regex.html', note: 'описывает regex, match и диапазоны совпадений' }, { label: 'PHP: preg_match_all', url: 'https://www.php.net/manual/en/function.preg-match-all.php', note: 'фиксирует исходный контракт PHP' }, { label: 'D language: strings', url: 'https://dlang.org/spec/arrays.html#strings', note: 'помогает сверить работу со строками и срезами' }],
|
||
},
|
||
'новый-движок-ctfe': {
|
||
title: 'D CTFE: вычислить константу на этапе компиляции и проверить границу применения',
|
||
excerpt: 'Что меняет новый движок CTFE в D и как отличить полезное вычисление на этапе компиляции от скрытого усложнения сборки.',
|
||
cover: '/assets/illustrations/ctfe-engine.svg',
|
||
problem: 'Функция вызывается как обычная, но разработчик не знает, когда выполняется её работа. Цена ошибки — медленная сборка, неожиданные ограничения CTFE или перенос тяжёлой операции в runtime.',
|
||
context: 'Материал о CTFE полезен, если показывает границу: компилятор может выполнить ограниченное вычисление, когда входы известны во время сборки. Это оптимизация и способ сформировать данные, а не обещание, что любой код станет compile-time.',
|
||
mechanism: ['CTFE выполняет функцию во время компиляции, если компилятор может интерпретировать её операции и все входы известны. Результат попадает в программу как готовое значение.', 'Ограничение проявляется на границе окружения: чтение сети, времени, произвольного файла или системного состояния нельзя считать стабильной частью константы. Если результат зависит от среды, его нужно получать явно при запуске.', 'Полезный тест смотрит на две вещи: программа собирается с заданным входом, а runtime не повторяет вычисление. Для сложной таблицы нужно сравнить выигрыш времени запуска со стоимостью компиляции и размером бинарника.'],
|
||
code: ['enum string[] routes = buildRoutes();', '', 'string[] buildRoutes() {', ' // Все входы известны компилятору.', ' return ["/", "/catalog", "/checkout"];', '}', '', 'static assert(routes.length == 3);', '', 'void main() {', ' // В runtime используем уже готовую таблицу.', ' import std.stdio : writeln;', ' writeln(routes);', '}'],
|
||
rows: [['Вход', 'Подходит для CTFE', 'Почему'], ['Литерал строки', 'Да', 'Результат зависит только от исходника'], ['Массив конфигурации', 'Да, если он фиксирован', 'Компилятор видит все значения'], ['Файл окружения', 'Нет как общий контракт', 'Содержимое меняется между средами'], ['Сеть и время', 'Нет', 'Результат не воспроизводим при сборке']],
|
||
steps: ['Определить, что именно известно до запуска программы.', 'Написать маленькую чистую функцию без скрытого окружения.', 'Добавить `static assert` для главного свойства результата.', 'Сравнить время сборки и размер бинарника до и после CTFE.', 'Проверить, что runtime использует готовое значение и не повторяет вычисление.', 'Оставить runtime-путь для данных, которые зависят от среды.'],
|
||
limits: ['Поддерживаемые операции CTFE расширяются между версиями компилятора.', 'Compile-time вычисление не делает секрет безопасным: значение может попасть в бинарник.', 'Большая таблица может ускорить запуск и одновременно замедлить сборку.'],
|
||
sources: [{ label: 'D language: CTFE', url: 'https://dlang.org/spec/function.html#ctfe', note: 'описывает условия выполнения функции при компиляции' }, { label: 'D: compile-time programming', url: 'https://dlang.org/blog/2017/04/10/the-new-ctfe-engine/', note: 'показывает исторический пример нового CTFE engine' }, { label: 'D: static assert', url: 'https://dlang.org/spec/expression.html#staticassert', note: 'фиксирует проверку свойства на этапе компиляции' }],
|
||
},
|
||
'особенности-vibe-d': {
|
||
title: 'Vibe.d: асинхронный сервер D начинается с границы владения запросом',
|
||
excerpt: 'Как читать Vibe.d не как набор удобных API, а как контракт event loop, обработчика запроса и внешних зависимостей.',
|
||
cover: '/assets/illustrations/vibe-api.svg',
|
||
problem: 'Асинхронный обработчик выглядит коротко, но скрывает блокирующую работу или не закрывает ресурс. Цена ошибки — занятые worker-ы, рост очереди и таймауты, которые проявляются только под нагрузкой.',
|
||
context: 'В исходном материале много полезных возможностей Vibe.d. Редактура сжимает перечень и оставляет то, что помогает принять решение: где живёт request context, какая операция блокирует loop и как закрыть соединение.',
|
||
mechanism: ['Vibe.d строит сервер вокруг event loop и асинхронных операций. `yield` и `async` освобождают текущую задачу только там, где библиотека действительно умеет ждать без блокировки потока.', 'Синхронная работа с диском, CPU или библиотекой без async-адаптера остаётся синхронной. Если выполнить её внутри обработчика, таймаут клиента станет симптомом занятого worker-а, а не проблемой HTTP.', 'Запрос должен иметь явный срок жизни. Таймер, отмена и обработка исключения должны закрыть или вернуть ресурс; иначе ошибки будут накапливаться в долгом процессе.'],
|
||
code: ['import vibe.http.server;', 'import vibe.http.router;', 'import vibe.data.json : serializeToJsonString;', '', 'auto router = new URLRouter;', 'router.get("/health", (HTTPServerRequest req,', ' HTTPServerResponse res) {', ' res.writeBody(`{"status":"ok"}`, "application/json");', '});', '', 'listenHTTP(new HTTPServerSettings, router);', 'runApplication();'],
|
||
rows: [['Операция', 'Владелец ожидания', 'Риск'], ['HTTP-запрос', 'Router и handler', 'Не обработанное исключение'], ['База', 'Async client или worker', 'Блокировка event loop'], ['Файл', 'Отдельный worker/async API', 'Долгий диск держит поток'], ['Таймаут', 'Контракт endpoint', 'Клиент ждёт дольше, чем сервер']],
|
||
steps: ['Назвать время жизни request и response.', 'Отделить быстрый handler от базы, файла и внешнего HTTP.', 'Для каждого ожидания определить async-операцию или worker.', 'Добавить timeout и обработчик исключения на границе запроса.', 'Проверить отмену и закрытие ресурсов при ошибке клиента.', 'Снять простой профиль очереди до вывода о производительности.'],
|
||
limits: ['API и idioms Vibe.d менялись между версиями; сверяйте документацию и версию компилятора.', 'Пример не является готовой production-конфигурацией TLS, прокси или логирования.', 'Асинхронная запись не гарантирует меньшую задержку, если узкое место — внешний сервис.'],
|
||
sources: [{ label: 'Vibe.d documentation', url: 'https://vibed.org/docs', note: 'описывает серверный API и асинхронную модель' }, { label: 'Vibe.d source', url: 'https://github.com/vibe-d/vibe.d', note: 'помогает сверить актуальные примеры и версии' }, { label: 'D language: fibers', url: 'https://dlang.org/library/core/thread/fiber.html', note: 'объясняет базовый механизм кооперативного переключения' }],
|
||
},
|
||
'вирус-самопроизвольный-запуск-брауз': {
|
||
title: 'Windows. Браузер сам открывает сайты: безопасная диагностика без запуска подозрительного файла',
|
||
excerpt: 'Порядок проверки автозапуска, расширений, прокси, DNS и задач Windows, когда браузер сам переходит на вредоносные страницы.',
|
||
cover: '/assets/illustrations/cover-virus.svg',
|
||
problem: 'Браузер сам открывает вкладки или перенаправляет поиск. Цена ошибки — потеря сессий и данных, а попытка удалить первый найденный файл может уничтожить доказательства и оставить автозапуск.',
|
||
context: 'Старую заметку о симптоме нельзя превращать в инструкцию по работе с вредоносным кодом. Безопасная версия ограничивается наблюдением, резервной копией, проверкой настроек и официальным сканером. Подозрительные файлы не запускаем и не публикуем.',
|
||
mechanism: ['Перенаправление может идти из расширения браузера, прокси, DNS, запланированной задачи, ярлыка или вредоносной программы. Один симптом не даёт права выбрать виновника.', 'Сначала фиксируем URL, время и активный профиль, затем отключаем сеть, если есть риск утечки. После этого проверяем расширения и настройки в чистом профиле, а системные изменения — через безопасный режим или доверенный антивирус.', 'Логи и образ диска важнее ручного удаления. Если на компьютере есть рабочие секреты, после изоляции их нужно отозвать с чистого устройства.'],
|
||
code: ['# PowerShell: только чтение настроек и задач', 'Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"', 'Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run"', 'Get-ScheduledTask | Where-Object {$_.State -ne "Disabled"}', 'Get-NetIPConfiguration', 'netsh winhttp show proxy', '', '# После сбора данных запустить Microsoft Defender Offline,', '# а не найденный неизвестный .exe.'],
|
||
rows: [['Слой', 'Наблюдение', 'Безопасное действие'], ['Браузер', 'Расширение или домашняя страница', 'Отключить неизвестное, проверить чистый профиль'], ['Сеть', 'Proxy/DNS отличаются от политики', 'Сверить с администратором и зафиксировать настройки'], ['Автозапуск', 'Run и Task Scheduler', 'Сохранить запись, затем удалить только подтверждённый entry'], ['Учётные данные', 'Сессии могли быть открыты', 'Отозвать токены с чистого устройства']],
|
||
steps: ['Записать симптом и отключить сетевую связь, если есть подозрение на утечку.', 'Не запускать и не пересылать подозрительный файл; сохранить его только по процедуре организации.', 'Проверить расширения и ярлык браузера в новом профиле.', 'Собрать список автозапуска, задач, proxy и DNS только на чтение.', 'Запустить официальный offline-сканер и передать результаты специалисту.', 'После очистки сменить пароли и отозвать сессии с заведомо чистого устройства.'],
|
||
limits: ['Команды PowerShell только читают конфигурацию; они не доказывают отсутствие вредоносного кода.', 'При корпоративном инциденте порядок организации и сохранение образа диска важнее ручной очистки.', 'Не отключайте защиту Windows и не публикуйте подозрительные образцы в статье или issue tracker.'],
|
||
sources: [{ label: 'Microsoft Defender Offline', url: 'https://support.microsoft.com/en-us/defender/troubleshoot-problems-with-detecting-and-removing-malware', note: 'описывает проверку вне запущенной Windows' }, { label: 'Mozilla Support: unwanted redirects', url: 'https://support.mozilla.org/en-US/kb/troubleshoot-firefox-issues-using-safe-mode', note: 'помогает отделить расширение от системной причины' }, { label: 'CISA: malware mitigation', url: 'https://www.cisa.gov/sites/default/files/publications/malware-threats-mitigation.pdf', note: 'фиксирует базовые действия при подозрении на вредоносное ПО' }],
|
||
},
|
||
'компиляция-64-x-разрядных-программ-на-dmd-по': {
|
||
title: 'DMD под Windows x64: проверить разрядность компилятора и бинарника',
|
||
excerpt: 'Как собрать 64-битную программу на DMD под Windows и не перепутать архитектуру компилятора с архитектурой исходного кода.',
|
||
cover: '/assets/illustrations/dmd-win64.svg',
|
||
problem: 'Команда сборки проходит, но бинарник не запускается на целевой системе или получает неверную зависимость. Цена ошибки — отлаживать код, когда причина находится в разрядности toolchain и DLL.',
|
||
context: 'Исходный рецепт `dmd -m64` остаётся отправной точкой. Для воспроизводимости нужно дополнительно зафиксировать версию DMD, linker, PATH и способ проверки PE-заголовка.',
|
||
mechanism: ['Флаг `-m64` задаёт архитектуру результата, но итог зависит и от установленного компилятора, линкера и библиотек. Наличие DMD в PATH не доказывает, что запускается ожидаемая версия.', '64-битный бинарник может не стартовать из-за отсутствующей DLL или несовместимого runtime. Поэтому после сборки проверяем не только размер файла, но и зависимости на чистой машине.', 'Debug и release могут использовать разные настройки и пути. Их нужно собирать в одинаково описанном окружении и не подменять ручным копированием DLL.'],
|
||
code: ['@echo off', 'where dmd', 'dmd --version', 'dub --version', '', 'dmd -m64 -of=bin\\app.exe source\\app.d', 'if errorlevel 1 exit /b 1', '', 'dumpbin /headers bin\\app.exe | findstr machine', 'dumpbin /dependents bin\\app.exe'],
|
||
rows: [['Проверка', 'Ожидаемое значение', 'Ошибка, которую ловит'], ['Compiler', 'Версия DMD зафиксирована', 'PATH указывает на другой toolchain'], ['Target', 'x64 в PE header', 'Собран x86-бинарник'], ['Runtime', 'Зависимости найдены', 'На чистой Windows нет DLL'], ['Build mode', 'debug/release назван явно', 'Сравниваются разные конфигурации']],
|
||
steps: ['Очистить PATH от неявных старых версий и вывести `where dmd`.', 'Зафиксировать DMD, linker и зависимости проекта.', 'Собрать маленький бинарник с `-m64` и проверить код возврата.', 'Проверить PE header и список DLL.', 'Запустить release на чистой или изолированной Windows-машине.', 'Сохранить команду сборки и версии рядом с артефактом.'],
|
||
limits: ['Флаги и поддерживаемые linkers зависят от версии DMD и Windows SDK.', '`dumpbin` входит в Visual Studio tools; на другой машине нужен эквивалентный PE-анализатор.', 'Размер файла не подтверждает разрядность и совместимость.'],
|
||
omitPatterns: [/^Файл разбит на разделы/i],
|
||
sources: [{ label: 'D language: download', url: 'https://dlang.org/download.html', note: 'показывает поддерживаемые версии компиляторов' }, { label: 'DMD command line', url: 'https://dlang.org/dmd-windows.html', note: 'описывает сборку DMD под Windows' }, { label: 'D language: specification', url: 'https://dlang.org/spec/', note: 'фиксирует правила языка и target-зависимые ограничения' }],
|
||
},
|
||
};
|
||
|
||
const revisions = Object.entries(cases).map(([slug, item]) => {
|
||
const source = archiveBySlug.get(slug);
|
||
if (!source) throw new Error('Legacy article not found: ' + slug);
|
||
const body = [
|
||
p(item.problem),
|
||
p(item.context),
|
||
figure(item.cover, item.title + ': схема границ проверки', 'Иллюстрация показывает границу между симптомом, техническим механизмом и проверяемым действием.'),
|
||
h2('Что сохраняем из исходной заметки'),
|
||
legacyBody(source, 6600, item.omitPatterns),
|
||
h2('Механизм без лишних обещаний'),
|
||
...item.mechanism.map(p),
|
||
h2('Минимальный воспроизводимый пример'),
|
||
p('Ниже — маленькая проверка, которую можно запустить или адаптировать в отдельном тестовом окружении. Значения демонстрационные; проектные идентификаторы, пути и версии нужно заменить своими и сохранить рядом с результатом.'),
|
||
codeBlock(item.code),
|
||
h2('Матрица диагностики'),
|
||
table(item.rows[0], item.rows.slice(1)),
|
||
h2('Порядок действий'),
|
||
orderedList(item.steps),
|
||
h2('Ограничения и безопасный следующий шаг'),
|
||
...item.limits.map(p),
|
||
p('После проверки должен остаться конкретный артефакт: вывод команды, тест, diff конфигурации или запись результата. Если его нет, формулировку нужно вернуть к симптому и не выдавать гипотезу за исправление.'),
|
||
h2('Что записать в ревью'),
|
||
p('Короткая запись должна отвечать на четыре вопроса: какой вход использовали, какой результат увидели, какая граница была проверена и какое действие разрешено дальше. Такая форма полезнее длинного вывода «всё работает»: другой инженер сможет повторить проверку и понять, где заканчивается пример.'),
|
||
p('Если результат зависит от версии Windows, PHP, Bitrix, D или браузера, версию фиксируем рядом с командой. Если проверка не охватывает сеть, production или реальные пользовательские данные, это ограничение пишем прямо. Тогда следующий шаг расширяет evidence, а не расширяет обещание.'),
|
||
h2('Проверяемые источники'),
|
||
sources(item.sources),
|
||
].join('\n');
|
||
const bodyCharacters = plainText(body.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*$/i, '')).length;
|
||
if (bodyCharacters < 5000 || bodyCharacters > 15000) {
|
||
throw new Error(slug + ': body length ' + bodyCharacters);
|
||
}
|
||
return {
|
||
slug,
|
||
title: item.title,
|
||
excerpt: item.excerpt,
|
||
categories: source.categories,
|
||
cover: item.cover,
|
||
readingMinutes: 12,
|
||
contentHtml: body,
|
||
proseLength: bodyCharacters,
|
||
};
|
||
});
|
||
|
||
export { revisions };
|
||
|
||
if (process.argv.includes('--print-revisions')) {
|
||
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
|
||
} else if (process.argv[1]?.endsWith('upgrade-legacy-archive.mjs')) {
|
||
console.log('Usage: node web/scripts/upgrade-legacy-archive.mjs --print-revisions');
|
||
}
|