This commit is contained in:
@@ -0,0 +1,900 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const webRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const articlesPath = join(webRoot, 'data', 'articles.json');
|
||||
|
||||
const sources = {
|
||||
bitrix: {
|
||||
label: 'Bitrix: CIBlockElement::Add',
|
||||
url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/add.php?print=Y',
|
||||
},
|
||||
phpUpload: {
|
||||
label: 'PHP: move_uploaded_file',
|
||||
url: 'https://www.php.net/move-uploaded-file',
|
||||
},
|
||||
phpErrors: {
|
||||
label: 'PHP: обработка ошибок',
|
||||
url: 'https://www.php.net/manual/en/function.set-error-handler.php',
|
||||
},
|
||||
mdnModules: {
|
||||
label: 'MDN: JavaScript modules',
|
||||
url: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules',
|
||||
},
|
||||
mdnFetch: {
|
||||
label: 'MDN: Fetch API',
|
||||
url: 'https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API',
|
||||
},
|
||||
mdnCache: {
|
||||
label: 'MDN: HTTP caching',
|
||||
url: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching',
|
||||
},
|
||||
webpack: {
|
||||
label: 'Webpack Guides',
|
||||
url: 'https://webpack.js.org/guides/',
|
||||
},
|
||||
docker: {
|
||||
label: 'Docker documentation',
|
||||
url: 'https://docs.docker.com/get-started/',
|
||||
},
|
||||
postgres: {
|
||||
label: 'PostgreSQL: Indexes',
|
||||
url: 'https://www.postgresql.org/docs/current/indexes.html',
|
||||
},
|
||||
redis: {
|
||||
label: 'Redis Streams',
|
||||
url: 'https://redis.io/docs/latest/develop/data-types/streams/',
|
||||
},
|
||||
dGc: {
|
||||
label: 'D: automatic memory management',
|
||||
url: 'https://dlang.org/spec/garbage.html',
|
||||
},
|
||||
dMemory: {
|
||||
label: 'D: core.memory',
|
||||
url: 'https://dlang.org/library/core/memory.html',
|
||||
},
|
||||
mdnPerformance: {
|
||||
label: 'MDN: Web performance',
|
||||
url: 'https://developer.mozilla.org/en-US/docs/Web/Performance',
|
||||
},
|
||||
wcag: {
|
||||
label: 'W3C: WCAG 2.2',
|
||||
url: 'https://www.w3.org/TR/WCAG22/',
|
||||
},
|
||||
owaspUpload: {
|
||||
label: 'OWASP: File Upload Cheat Sheet',
|
||||
url: 'https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html',
|
||||
},
|
||||
owaspCsrf: {
|
||||
label: 'OWASP: CSRF Prevention Cheat Sheet',
|
||||
url: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html',
|
||||
},
|
||||
owaspHtml: {
|
||||
label: 'OWASP: HTML5 Security Cheat Sheet',
|
||||
url: 'https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html',
|
||||
},
|
||||
otel: {
|
||||
label: 'OpenTelemetry: signals',
|
||||
url: 'https://opentelemetry.io/docs/concepts/signals/',
|
||||
},
|
||||
playwright: {
|
||||
label: 'Playwright: best practices',
|
||||
url: 'https://playwright.dev/docs/best-practices',
|
||||
},
|
||||
kubernetes: {
|
||||
label: 'Kubernetes: Deployments',
|
||||
url: 'https://kubernetes.io/docs/concepts/workloads/controllers/deployment/',
|
||||
},
|
||||
nistAi: {
|
||||
label: 'NIST: Generative AI Profile',
|
||||
url: 'https://www.nist.gov/publications/artificial-intelligence-risk-management-framework-generative-artificial-intelligence',
|
||||
},
|
||||
};
|
||||
|
||||
const existingSlots = new Set([
|
||||
'2018-10-practice',
|
||||
'2019-01-practice',
|
||||
]);
|
||||
|
||||
const articleKinds = ['practice', 'mechanism', 'field'];
|
||||
|
||||
function theme(year, month, id, area, subject, problem, model, field, family, sourceKeys, categories) {
|
||||
return { year, month, id, area, subject, problem, model, field, family, sourceKeys, categories };
|
||||
}
|
||||
|
||||
const themes = [
|
||||
theme(2018, 1, 'bitrix-elements', 'Bitrix API', 'жизненный цикл элемента инфоблока', 'создать элемент так, чтобы обязательные поля, свойства и ошибки не потерялись', 'добавление элемента — это не только вызов Add, но и контракт данных, событий и проверки результата', 'элемент появился в админке, но не стал частью ожидаемого пользовательского сценария', 'bitrix', ['bitrix', 'phpErrors'], ['Bitrix', 'PHP']),
|
||||
theme(2018, 2, 'php-diagnostics', 'PHP', 'диагностика ошибок интеграции', 'увидеть исходную ошибку, а не только пустой ответ или false', 'ошибка должна иметь контекст: входные данные, идентификатор операции, место возникновения и безопасный журнал', 'интеграция падает только у одного клиента и воспроизвести ситуацию сразу не получается', 'php', ['phpErrors'], ['PHP', 'Диагностика']),
|
||||
theme(2018, 3, 'safe-uploads', 'PHP', 'безопасная загрузка файлов', 'принять изображение от пользователя, не превратив загрузку в вход для чужого кода', 'имя, MIME-тип, размер, права и место хранения проверяются раздельно', 'картинка выглядит безобидно, но сервер не должен доверять расширению файла', 'php', ['phpUpload', 'owaspUpload'], ['PHP', 'Безопасность']),
|
||||
theme(2018, 4, 'bitrix-slugs', 'Bitrix API', 'символьные коды и URL', 'получить читаемый и уникальный код элемента из пользовательского названия', 'транслитерация даёт основу, а уникальность и нормализация делают адрес устойчивым', 'два похожих товара пытаются занять один URL и ломают ссылку из каталога', 'bitrix', ['bitrix'], ['Bitrix', 'PHP', 'SEO']),
|
||||
theme(2018, 5, 'legacy-jquery', 'JavaScript', 'старый код на jQuery', 'подключить новый код к старому интерфейсу и не получить несколько обработчиков на одну кнопку', 'граница между глобальным DOM-кодом и модулем должна быть небольшой и наблюдаемой', 'после точечной правки форма начинает отправляться дважды', 'js', ['mdnModules', 'webpack'], ['JavaScript', 'jQuery']),
|
||||
theme(2018, 6, 'webpack-entry', 'Webpack', 'точки входа и зависимости', 'собрать проект так, чтобы общие библиотеки и код страницы не дублировались', 'entry-файл задаёт границу загрузки, а граф импортов показывает реальную зависимость модулей', 'после новой сборки bundle неожиданно стал вдвое больше', 'js', ['webpack', 'mdnModules'], ['JavaScript', 'Webpack']),
|
||||
theme(2018, 7, 'http-timeouts', 'HTTP', 'таймауты в веб-интеграции', 'отличить долгий ответ от зависшего процесса и выбрать корректный срок ожидания', 'таймауты клиента, прокси, приложения и базы — это разные уровни одного запроса', 'браузер ждёт достаточно долго, но сервер всё равно обрывает выгрузку', 'http', ['mdnFetch', 'mdnCache'], ['HTTP', 'PHP']),
|
||||
theme(2018, 8, 'tls-ca', 'PHP', 'TLS и CA bundle', 'починить проверку сертификата без отключения проверки SSL', 'клиент доверяет цепочке сертификатов, а не просто доменному имени в адресной строке', 'сайт открывается в браузере, но cURL возвращает certificate error', 'http', ['phpUpload', 'owaspHtml'], ['PHP', 'SSL']),
|
||||
theme(2018, 9, 'windows-dev-env', 'Windows', 'окружение разработчика', 'сделать локальную среду воспроизводимой, а не зависящей от случайных настроек компьютера', 'версия интерпретатора, расширения, PATH и конфигурация должны быть описаны рядом с проектом', 'у коллеги тот же проект не запускается, хотя код не менялся', 'ops', ['docker'], ['Windows', 'Инструменты']),
|
||||
theme(2018, 10, 'image-workflow', 'Bitrix API', 'работа с изображениями в форме', 'связать UI-редактор, проверку файла и сохранение привязки к сущности', 'редактор помогает пользователю, но права, тип файла и жизненный цикл данных остаются на сервере', 'картинка успешно выбрана, но после сохранения страницы у сущности остаётся пустое поле', 'bitrix', ['bitrix', 'owaspUpload'], ['Bitrix', 'PHP']),
|
||||
theme(2018, 11, 'php-integration-tests', 'PHP', 'интеграционные проверки', 'проверить взаимодействие приложения, файловой системы и внешнего HTTP-клиента', 'ценность интеграционного теста в том, что он проверяет контракт между частями системы', 'модульные тесты зелёные, а реальная форма ломается после отправки файла', 'test', ['phpUpload', 'mdnFetch'], ['PHP', 'Тестирование']),
|
||||
theme(2018, 12, 'legacy-refactoring', 'Разработка', 'рефакторинг Bitrix-проекта', 'изменять старый проект маленькими безопасными шагами', 'рефакторинг — это управление риском через границы, проверки и короткие циклы обратной связи', 'задача кажется простой, пока не выясняется, что функцию вызывают из пяти шаблонов', 'writing', ['bitrix', 'webpack'], ['Bitrix', 'Рефакторинг']),
|
||||
|
||||
theme(2019, 1, 'jquery-webpack', 'JavaScript', 'jQuery в Webpack', 'сделать зависимость явной и не потерять плагины, ожидающие window.jQuery', 'модульная система и глобальные переменные имеют разные правила видимости', 'после миграции сборки старый inputmask перестаёт видеть jQuery', 'js', ['webpack', 'mdnModules'], ['JavaScript', 'Webpack']),
|
||||
theme(2019, 2, 'es-modules', 'JavaScript', 'ES-модули в браузере', 'разделить код на файлы без хаоса в глобальной области видимости', 'модуль загружается как отдельный ресурс и подчиняется правилам CORS и графа импортов', 'страница работает на сервере, но падает при открытии через file://', 'js', ['mdnModules'], ['JavaScript']),
|
||||
theme(2019, 3, 'event-loop', 'JavaScript', 'event loop и асинхронность', 'понять, почему обработчик интерфейса «подвисает», хотя запросы асинхронные', 'обещание не делает CPU-вычисление параллельным: главный поток всё ещё один', 'кнопка реагирует только после обработки большого массива в then', 'js', ['mdnFetch'], ['JavaScript', 'Асинхронность']),
|
||||
theme(2019, 4, 'forms-validation', 'Frontend', 'валидация форм', 'дать человеку понятную ошибку и всё равно проверить данные на сервере', 'клиентская валидация улучшает UX, но сервер остаётся источником истины', 'поле подсвечено зелёным, а API всё равно отказывается принимать значение', 'frontend', ['wcag', 'owaspHtml'], ['JavaScript', 'UX']),
|
||||
theme(2019, 5, 'http-caching', 'HTTP', 'кеширование HTTP-ответов', 'ускорить повторные запросы, не показывая пользователю устаревшие данные', 'кеш — это договор о свежести, ключе и инвалидировании, а не только заголовок max-age', 'после деплоя клиент продолжает получать старый JavaScript-файл', 'http', ['mdnCache'], ['HTTP', 'Производительность']),
|
||||
theme(2019, 6, 'rest-api', 'API', 'контракт REST API', 'сделать интерфейс предсказуемым для формы, скрипта и внешнего клиента', 'контракт включает не только URL, но и статусы, ошибки, идемпотентность и формат полей', 'одна и та же ошибка то приходит строкой, то пустым массивом', 'http', ['mdnFetch'], ['API', 'HTTP']),
|
||||
theme(2019, 7, 'sql-indexes', 'SQL', 'индексы и планы запросов', 'ускорить нужный запрос, а не поставить индекс на каждую колонку подряд', 'индекс ускоряет поиск ценой записи, места и усложнения плана', 'страница каталога медленно открывается только при большом количестве товаров', 'sql', ['postgres'], ['SQL', 'Производительность']),
|
||||
theme(2019, 8, 'frontend-performance', 'Frontend', 'производительность первой загрузки', 'измерить, что именно мешает странице стать полезной пользователю', 'сеть, парсинг, выполнение JavaScript и рендеринг образуют одну цепочку задержки', 'страница быстро отвечает сервером, но визуально остаётся пустой', 'frontend', ['mdnPerformance'], ['JavaScript', 'Производительность']),
|
||||
theme(2019, 9, 'accessibility-basics', 'Frontend', 'базовая доступность', 'дать интерфейсу нормальную семантику, фокус и текстовые подписи', 'браузер строит не только DOM, но и дерево доступности для вспомогательных технологий', 'красивая кнопка работает мышью, но недоступна с клавиатуры', 'frontend', ['wcag'], ['Доступность', 'Frontend']),
|
||||
theme(2019, 10, 'typescript-migration', 'JavaScript', 'переход на TypeScript', 'добавить типы без переписывания всей кодовой базы', 'типы полезнее всего на границах данных: API, форма, конфигурация и публичная функция', 'старый JavaScript-модуль используют в десятке страниц с разными аргументами', 'js', ['mdnModules', 'webpack'], ['JavaScript', 'TypeScript']),
|
||||
theme(2019, 11, 'reproducible-builds', 'Сборка', 'воспроизводимая сборка', 'получать одинаковый результат на локальной машине и в CI', 'lockfile, версии среды и явная конфигурация превращают сборку из ритуала в процесс', 'после npm install у одного разработчика появляется ошибка, которой нет у остальных', 'ops', ['webpack', 'docker'], ['JavaScript', 'Инструменты']),
|
||||
theme(2019, 12, 'code-ownership', 'Команда', 'ответственность за код', 'сделать передачу модуля понятной, а не держать знания только в голове автора', 'сопровождаемость начинается там, где другой человек способен воспроизвести решение', 'автор старой интеграции ушёл, а никто не понимает, какие поля обязательны', 'writing', ['bitrix', 'webpack'], ['Разработка', 'Команда']),
|
||||
|
||||
theme(2020, 1, 'docker-local', 'Docker', 'локальная разработка в контейнере', 'запускать приложение с одинаковыми зависимостями у всей команды', 'образ описывает окружение, а контейнер запускает его как изолированный процесс', 'проект требует старую версию PHP, которой нет в системе разработчика', 'docker', ['docker'], ['Docker', 'Инфраструктура']),
|
||||
theme(2020, 2, 'configs-secrets', 'Конфигурация', 'настройки и секреты', 'разделить код, параметры окружения и чувствительные данные', 'конфигурация меняется между средами, а секрет не должен попадать в репозиторий или лог', 'боевой токен случайно оказался в JSON-ответе об ошибке', 'ops', ['docker', 'owaspHtml'], ['Безопасность', 'DevOps']),
|
||||
theme(2020, 3, 'ci-pipeline', 'CI/CD', 'минимальный pipeline', 'превратить проверку кода в повторяемую последовательность шагов', 'CI ценен не скоростью сам по себе, а тем, что одинаково проверяет каждое изменение', 'локально тесты запускались, но после merge приложение не собирается', 'test', ['docker', 'playwright'], ['CI/CD', 'Тестирование']),
|
||||
theme(2020, 4, 'reverse-proxy', 'HTTP', 'reverse proxy перед приложением', 'разобраться, где завершается TLS и кто отвечает за таймауты и заголовки', 'прокси отделяет внешнее HTTP-соединение от процесса приложения и задаёт границу ответственности', 'приложение считает запрос HTTPS, а в логах почему-то видит http', 'http', ['mdnCache', 'docker'], ['HTTP', 'Инфраструктура']),
|
||||
theme(2020, 5, 'background-jobs', 'Backend', 'фоновые задачи', 'вынести долгую работу из пользовательского HTTP-запроса', 'запрос должен принять намерение, а worker — выполнить тяжёлую часть с отдельным состоянием', 'экспорт отчёта занимает минуту и держит браузер пользователя в ожидании', 'redis', ['redis', 'docker'], ['Backend', 'Очереди']),
|
||||
theme(2020, 6, 'retry-idempotency', 'Backend', 'повтор запросов и идемпотентность', 'повторять временные сбои, не создавая второй платёж, заказ или письмо', 'повтор безопасен только тогда, когда операция имеет ключ идемпотентности и понятный результат', 'клиент не получил ответ и отправил ту же команду ещё раз', 'http', ['mdnFetch'], ['API', 'Надёжность']),
|
||||
theme(2020, 7, 'structured-logs', 'Наблюдаемость', 'структурированные логи', 'находить нужный запрос по идентификатору, а не читать тысячи строк вручную', 'лог — это событие с полями, уровнем и контекстом, а не просто текст для консоли', 'в журнале есть ошибка, но непонятно, к какому пользователю и заказу она относится', 'telemetry', ['otel'], ['Наблюдаемость', 'Backend']),
|
||||
theme(2020, 8, 'metrics-basics', 'Наблюдаемость', 'метрики приложения', 'отличать отдельную ошибку от ухудшения поведения всей системы', 'метрика показывает изменение во времени, поэтому важны единицы, лейблы и базовая линия', 'жалобы пользователей есть, а в логах нет явной аварии', 'telemetry', ['otel'], ['Наблюдаемость', 'Производительность']),
|
||||
theme(2020, 9, 'tracing-basics', 'Наблюдаемость', 'трассировка запроса', 'увидеть путь одного запроса через frontend, API и внешнюю систему', 'trace связывает операции общим контекстом и показывает задержки между компонентами', 'один endpoint иногда отвечает десять секунд, но причина меняется от случая к случаю', 'telemetry', ['otel'], ['Наблюдаемость', 'HTTP']),
|
||||
theme(2020, 10, 'backup-recovery', 'Данные', 'резервное копирование и восстановление', 'проверять не наличие бэкапа, а возможность вернуть данные в рабочее состояние', 'копия ценна только после регулярной проверки восстановления и понятного RPO/RTO', 'резервная копия есть, но никто не пробовал поднять из неё тестовую базу', 'sql', ['postgres', 'docker'], ['Данные', 'Надёжность']),
|
||||
theme(2020, 11, 'security-baseline', 'Безопасность', 'минимальная защита веб-приложения', 'закрыть наиболее очевидные дыры в аутентификации, вводе и настройках', 'безопасность — это серия независимых барьеров, а не одна «волшебная» проверка', 'старый административный endpoint доступен без CSRF-защиты', 'security', ['owaspCsrf', 'owaspHtml'], ['Безопасность', 'Web']),
|
||||
theme(2020, 12, 'incident-review', 'Надёжность', 'разбор инцидента', 'извлечь из сбоя изменение процесса, а не найти виноватого', 'хороший разбор связывает симптом, временную шкалу, защитные механизмы и действие', 'сервис восстановили, но через месяц та же ошибка повторилась', 'writing', ['otel', 'docker'], ['Надёжность', 'Команда']),
|
||||
|
||||
theme(2021, 1, 'transactions', 'PostgreSQL', 'границы транзакции', 'сохранить согласованность нескольких изменений данных', 'транзакция фиксирует набор изменений целиком или откатывает его при ошибке', 'заказ создан, а строка остатка на складе не обновилась', 'sql', ['postgres'], ['PostgreSQL', 'Данные']),
|
||||
theme(2021, 2, 'cache-invalidation', 'Кеширование', 'инвалидация кеша', 'ускорить чтение и не сделать старые данные постоянной правдой', 'кеш всегда требует владельца, ключа, срока жизни и события обновления', 'пользователь поменял профиль, а соседняя страница ещё час показывает старое имя', 'redis', ['redis', 'mdnCache'], ['Кеширование', 'Backend']),
|
||||
theme(2021, 3, 'queues', 'Очереди', 'очереди задач', 'распределить работу между worker-процессами и не потерять задачу при падении', 'очередь отделяет приём работы от её выполнения и требует подтверждения обработки', 'один worker завис, а задачи после него перестали двигаться', 'redis', ['redis'], ['Очереди', 'Backend']),
|
||||
theme(2021, 4, 'data-consistency', 'Архитектура', 'согласованность данных между сервисами', 'не обещать мгновенную синхронность там, где система работает асинхронно', 'между локальной транзакцией и результатом в другом сервисе существует временная граница', 'пользователь видит заказ, но уведомление о нём приходит через несколько минут', 'redis', ['redis', 'postgres'], ['Архитектура', 'Данные']),
|
||||
theme(2021, 5, 'distributed-locks', 'Backend', 'блокировки и конкуренция', 'защитить операцию, которую нельзя выполнить одновременно дважды', 'блокировка имеет область, срок жизни и сценарий потери владельца', 'две вкладки одновременно уменьшают один и тот же остаток', 'redis', ['redis', 'postgres'], ['Backend', 'Конкурентность']),
|
||||
theme(2021, 6, 'event-driven', 'Архитектура', 'событийная интеграция', 'сделать уведомление о факте отдельным от реакции конкретного потребителя', 'событие описывает уже произошедший факт и может быть обработано несколькими независимыми получателями', 'добавление нового письма требует менять основной код оформления заказа', 'redis', ['redis'], ['Архитектура', 'События']),
|
||||
theme(2021, 7, 'search-indexing', 'Поиск', 'индексация и поиск', 'отделить поиск по тексту от транзакционного хранения сущностей', 'индекс оптимизирован под поиск, поэтому обновляется по своему контракту и может отставать', 'новый товар есть в карточке, но не находится через поиск несколько минут', 'sql', ['postgres'], ['Поиск', 'Данные']),
|
||||
theme(2021, 8, 'storage-contracts', 'Данные', 'контракт хранилища', 'выбрать место для файла, записи и временного объекта по их жизненному циклу', 'способ хранения определяется доступом, сроком жизни, размером, резервированием и правами', 'в базе лежат гигабайты изображений, а их нельзя удобно отдать пользователю', 'ops', ['owaspUpload', 'docker'], ['Данные', 'Инфраструктура']),
|
||||
theme(2021, 9, 'api-versioning', 'API', 'версионирование API', 'изменять контракт так, чтобы старые клиенты не ломались внезапно', 'версия — это договор об обратной совместимости, а не только сегмент URL', 'мобильное приложение ещё отправляет поле, которое backend уже переименовал', 'http', ['mdnFetch'], ['API', 'Архитектура']),
|
||||
theme(2021, 10, 'd-runtime-service', 'D', 'runtime D в сервисе', 'использовать сильные стороны D, не игнорируя работу GC и границы ввода-вывода', 'низкоуровневая модель полезна тогда, когда измерена, а не включена из любопытства', 'сервис быстрый в тесте, но под нагрузкой появляются редкие паузы', 'd', ['dGc', 'dMemory'], ['DLang', 'Backend']),
|
||||
theme(2021, 11, 'load-testing', 'Нагрузка', 'нагрузочное тестирование', 'проверять систему по сценарию пользователя, а не по одному красивому RPS', 'нагрузка раскрывает очереди, лимиты соединений, блокировки и узкие места зависимостей', 'на стенде всё быстро, а в день акции время ответа растёт в несколько раз', 'telemetry', ['otel', 'postgres'], ['Нагрузка', 'Надёжность']),
|
||||
theme(2021, 12, 'architecture-review', 'Архитектура', 'архитектурный разбор решения', 'зафиксировать ключевые допущения до того, как они станут скрытым долгом', 'архитектурное решение — это контекст, вариант, последствия и дата пересмотра', 'команда спорит о технологии, но не сформулировала, какую проблему выбирает решать', 'writing', ['kubernetes', 'postgres'], ['Архитектура', 'Команда']),
|
||||
|
||||
theme(2022, 1, 'ssr-csr', 'Frontend', 'границы SSR и CSR', 'выбрать, что показать пользователю сразу, а что оставить клиентскому коду', 'серверный и клиентский рендер решают разные проблемы времени ответа, интерактивности и кеширования', 'контент важен для первого экрана, но виджет требует браузерного API', 'frontend', ['mdnPerformance', 'mdnModules'], ['Frontend', 'Производительность']),
|
||||
theme(2022, 2, 'performance-budget', 'Frontend', 'бюджет производительности', 'превратить пожелание «быстрее» в проверяемые ограничения для страницы', 'бюджет фиксирует допустимые размеры, задержки и сценарии измерения до регрессии', 'каждая новая библиотека кажется маленькой, а первый экран постепенно тяжелеет', 'frontend', ['mdnPerformance'], ['Производительность', 'Frontend']),
|
||||
theme(2022, 3, 'browser-rendering', 'Frontend', 'как браузер рисует страницу', 'понимать, почему изменение DOM иногда дорого, а правильный порядок ресурсов важен', 'парсинг, стили, layout, paint и JavaScript влияют друг на друга через критический путь', 'простая анимация начинает дёргаться после добавления большого списка', 'frontend', ['mdnPerformance'], ['JavaScript', 'Производительность']),
|
||||
theme(2022, 4, 'accessible-interface', 'Доступность', 'доступный интерфейс', 'проверять фокус, подписи, контраст и сценарий без мыши как обычную часть разработки', 'доступность строится на семантике и ожидаемом поведении, а не на отдельном виджете для проверки', 'форма красивая, но screen reader не связывает подпись с полем', 'frontend', ['wcag'], ['Доступность', 'Frontend']),
|
||||
theme(2022, 5, 'design-system', 'Frontend', 'маленькая дизайн-система', 'снизить число случайных вариантов кнопок, ошибок и полей в продукте', 'компонент становится полезным, когда у него есть контракт поведения, состояний и доступности', 'у трёх форм разные обязательные поля, цвета ошибок и логика disabled', 'frontend', ['wcag'], ['Frontend', 'Дизайн-система']),
|
||||
theme(2022, 6, 'form-errors', 'UX', 'ошибки формы', 'вернуть пользователю действие, а не только факт неудачи', 'хорошая ошибка привязана к полю, объясняет причину и не теряет уже введённые данные', 'после серверной ошибки форма очищается и человек не понимает, что исправлять', 'frontend', ['wcag', 'owaspHtml'], ['UX', 'Frontend']),
|
||||
theme(2022, 7, 'poor-network', 'Web', 'работа при плохой сети', 'сделать интерфейс честным при медленном, нестабильном или отсутствующем соединении', 'сеть может потерять запрос после отправки, поэтому UI должен различать ожидание, ошибку и неизвестный результат', 'кнопка сохранения нажимается дважды, пока пользователь не видит ответа', 'http', ['mdnFetch', 'mdnCache'], ['Web', 'UX']),
|
||||
theme(2022, 8, 'media-performance', 'Frontend', 'изображения и медиа', 'отдавать нужный размер файла и не заставлять браузер скачивать лишнее', 'медиа влияет на сеть, layout и визуальную стабильность страницы', 'карточка товара занимает большую часть трафика, хотя изображение выводится маленьким', 'frontend', ['mdnPerformance', 'owaspUpload'], ['Frontend', 'Производительность']),
|
||||
theme(2022, 9, 'mobile-ux', 'UX', 'мобильный сценарий', 'проектировать действие под палец, маленький экран и краткое внимание', 'мобильный интерфейс ограничен не только размером, но и сетью, вводом и контекстом пользователя', 'десктопная таблица на телефоне превращается в стену мелкого текста', 'frontend', ['wcag', 'mdnPerformance'], ['UX', 'Frontend']),
|
||||
theme(2022, 10, 'ui-tests', 'Тестирование', 'проверки пользовательского сценария', 'автоматизировать путь человека через интерфейс, а не набор технических селекторов', 'устойчивый e2e-тест ждёт наблюдаемое состояние и проверяет результат действия', 'тест кликает по кнопке, но иногда падает из-за случайного тайминга', 'test', ['playwright'], ['Тестирование', 'Frontend']),
|
||||
theme(2022, 11, 'bitrix-performance', 'Bitrix', 'производительность Bitrix-страницы', 'найти запрос, шаблон или кеш, который реально влияет на время ответа', 'быстрый сайт требует измерять цепочку целиком: база, PHP, кеш, HTML и браузер', 'страница каталога тормозит, но разработчики спорят только о размере JavaScript', 'bitrix', ['mdnPerformance', 'postgres'], ['Bitrix', 'Производительность']),
|
||||
theme(2022, 12, 'user-research', 'Продукт', 'исследование пользовательской проблемы', 'связать техническое изменение с наблюдаемым сценарием человека', 'инженерная гипотеза полезна, когда у неё есть пользователь, контекст, сигнал и способ проверки', 'команда хочет переписать экран, но не знает, на каком шаге люди действительно уходят', 'writing', ['mdnPerformance', 'wcag'], ['Продукт', 'UX']),
|
||||
|
||||
theme(2023, 1, 'threat-model', 'Безопасность', 'модель угроз', 'обсудить, что именно защищаем, от кого и какими границами', 'модель угроз связывает актив, актёра, поверхность атаки и меру защиты', 'команда спорит о шифровании, но не знает, какие данные вообще считаются чувствительными', 'security', ['owaspHtml', 'owaspCsrf'], ['Безопасность']),
|
||||
theme(2023, 2, 'sessions-auth', 'Безопасность', 'аутентификация и сессии', 'не путать подтверждение личности с правом выполнить конкретное действие', 'сессия, cookie, срок жизни и серверная проверка образуют один контур доверия', 'пользователь разлогинился в одной вкладке, а в другой ещё может сохранить данные', 'security', ['owaspCsrf', 'owaspHtml'], ['Безопасность', 'Аутентификация']),
|
||||
theme(2023, 3, 'csrf-cors', 'Безопасность', 'CSRF и CORS', 'не открывать доступ к пользовательской сессии только потому, что API нужен frontend-виджету', 'CORS управляет доступом браузерного кода, а CSRF защищает действие от подделанного запроса', 'сервер отдаёт Access-Control-Allow-Origin: * вместе с cookies', 'security', ['owaspCsrf', 'owaspHtml'], ['Безопасность', 'HTTP']),
|
||||
theme(2023, 4, 'dependency-security', 'Безопасность', 'безопасность зависимостей', 'понимать, какие библиотеки действительно попадают в production и как их обновлять', 'зависимость — это часть цепочки поставки, поэтому версия, источник и обновление имеют значение', 'уязвимость найдена в пакете, который уже не используется в исходном коде напрямую', 'ops', ['webpack', 'docker'], ['Безопасность', 'Зависимости']),
|
||||
theme(2023, 5, 'static-analysis', 'Качество', 'статический анализ', 'ловить повторяемые ошибки до запуска приложения', 'линтер и анализатор полезны там, где правила однозначны и проверяются автоматически', 'одна и та же ошибка с null и необработанным Promise появляется в разных модулях', 'test', ['playwright', 'mdnModules'], ['Качество', 'JavaScript']),
|
||||
theme(2023, 6, 'secrets-supply-chain', 'Безопасность', 'секреты и цепочка поставки', 'не давать токенам попасть в логи, образы, историю git и временные файлы CI', 'секрет безопасен только пока ограничены его распространение, срок жизни и область действия', 'токен тестовой среды случайно сохранился в образе контейнера', 'ops', ['docker', 'owaspHtml'], ['Безопасность', 'DevOps']),
|
||||
theme(2023, 7, 'contract-tests', 'Тестирование', 'контрактные тесты API', 'ловить расхождение producer и consumer до общего релиза', 'контракт фиксирует поля, статусы и примеры, которые обе стороны считают допустимыми', 'backend поменял nullable-поле, а frontend узнал об этом только после выкладки', 'test', ['mdnFetch', 'playwright'], ['Тестирование', 'API']),
|
||||
theme(2023, 8, 'e2e-stability', 'Тестирование', 'стабильные e2e-тесты', 'проверять сценарий без sleep, случайных селекторов и зависимости от чужих данных', 'устойчивость теста строится на изоляции данных, пользовательских локаторах и наблюдаемом результате', 'падение теста невозможно повторить, потому что оно зависит от порядка запуска', 'test', ['playwright'], ['Тестирование', 'Frontend']),
|
||||
theme(2023, 9, 'telemetry-signals', 'Наблюдаемость', 'логи, метрики и трассы', 'использовать три сигнала вместе вместо бесконечного поиска в одном журнале', 'логи объясняют событие, метрики показывают тенденцию, а trace связывает путь запроса', 'график показывает рост ошибок, но не говорит, где именно теряется запрос', 'telemetry', ['otel'], ['Наблюдаемость']),
|
||||
theme(2023, 10, 'sli-slo', 'Надёжность', 'SLI и SLO', 'согласовать, какое качество сервиса действительно важно для пользователя', 'метрика становится целью только после того, как определены окно измерения и допустимый уровень ошибки', 'команда считает доступность по разным формулам и спорит, был ли инцидент', 'telemetry', ['otel'], ['Надёжность', 'Наблюдаемость']),
|
||||
theme(2023, 11, 'postmortem', 'Надёжность', 'postmortem без поиска виноватого', 'сделать из сбоя улучшение системы, а не публичную казнь', 'разбор должен объяснить, почему защита не сработала, и что изменится в коде или процессе', 'в отчёте есть фамилия виноватого, но нет действия, которое предотвратит повтор', 'writing', ['otel', 'playwright'], ['Надёжность', 'Команда']),
|
||||
theme(2023, 12, 'security-audit', 'Безопасность', 'практический аудит веб-проекта', 'пройти по поверхности атаки системно и зафиксировать приоритеты исправления', 'аудит — это инвентаризация, проверка контроля и доказательство результата, а не разовый сканер', 'команда получила длинный список предупреждений и не знает, что исправлять первым', 'security', ['owaspUpload', 'owaspCsrf', 'owaspHtml'], ['Безопасность']),
|
||||
|
||||
theme(2024, 1, 'legacy-modernization', 'Архитектура', 'модернизация legacy-системы', 'выбрать узкий участок для улучшения, не останавливая весь продукт', 'старую систему меняют через границы и совместимые переходы, а не через одномоментную замену', 'проекту десять лет, и мысль о полном переписывании повторяется каждый квартал', 'writing', ['docker', 'postgres'], ['Рефакторинг', 'Архитектура']),
|
||||
theme(2024, 2, 'modular-monolith', 'Архитектура', 'модульный монолит', 'сделать внутренние границы явными до выделения сервисов', 'модуль определяется владением данными, API и зависимостями, а не папкой в репозитории', 'две команды меняют одну таблицу и случайно ломают друг другу сценарии', 'writing', ['postgres', 'kubernetes'], ['Архитектура', 'Backend']),
|
||||
theme(2024, 3, 'package-boundaries', 'Разработка', 'границы пакетов', 'не дать общему пакету превратиться в свалку случайных утилит', 'публичный API пакета должен быть меньше его внутреннего устройства', 'один helper тянет в frontend половину backend-зависимостей', 'js', ['mdnModules', 'webpack'], ['JavaScript', 'Архитектура']),
|
||||
theme(2024, 4, 'data-migrations', 'Данные', 'безопасная миграция данных', 'изменить схему и данные так, чтобы старая и новая версия приложения могли жить рядом', 'хорошая миграция разделяет добавление, заполнение, переключение чтения и удаление старого', 'новая колонка появилась, но старый сервис ещё не умеет её заполнять', 'sql', ['postgres'], ['Данные', 'Миграции']),
|
||||
theme(2024, 5, 'platform-templates', 'Платформа', 'шаблоны для команд', 'убрать повторяемую настройку без превращения платформы в обязательный лабиринт', 'платформенный шаблон должен давать безопасный старт и оставлять понятные точки расширения', 'каждая новая команда вручную копирует CI, Dockerfile и healthcheck', 'platform', ['docker', 'kubernetes'], ['Платформа', 'DevOps']),
|
||||
theme(2024, 6, 'container-orchestration', 'Kubernetes', 'управление контейнерной нагрузкой', 'отделить описание желаемого состояния сервиса от ручного запуска процессов', 'Deployment поддерживает нужное количество реплик и контролируемо заменяет старые версии', 'после обновления образа часть pod-ов работает на старой версии слишком долго', 'platform', ['kubernetes', 'docker'], ['Kubernetes', 'DevOps']),
|
||||
theme(2024, 7, 'release-engineering', 'Релизы', 'инженерия релиза', 'сделать выпуск версией с проверяемым содержимым и планом отката', 'релиз связывает commit, артефакт, конфигурацию, миграции и наблюдение после выкладки', 'код в main уже другой, чем образ, который фактически работает в production', 'platform', ['docker', 'kubernetes'], ['Релизы', 'DevOps']),
|
||||
theme(2024, 8, 'feature-flags', 'Продукт', 'feature flags', 'отделить выкладку кода от включения поведения для аудитории', 'флаг — это временный контракт с владельцем, условиями включения и датой удаления', 'новая функция работает у сотрудников, но должна быть выключена для всех клиентов', 'ops', ['kubernetes'], ['Продукт', 'DevOps']),
|
||||
theme(2024, 9, 'adr-decisions', 'Архитектура', 'запись инженерного решения', 'сохранить контекст выбора, чтобы через год не спорить с неизвестным прошлым', 'ADR описывает проблему, принятое решение, альтернативы и последствия', 'в коде есть необычный обходной путь, но никто не помнит, какую аварию он предотвращает', 'writing', ['postgres', 'kubernetes'], ['Архитектура', 'Документация']),
|
||||
theme(2024, 10, 'capacity-cost', 'Инфраструктура', 'ёмкость и стоимость', 'измерять потребление ресурсов до того, как оптимизация станет срочной', 'стоимость растёт из нагрузки, хранения, трафика и операционной сложности', 'счёт за инфраструктуру вырос, хотя число пользователей почти не изменилось', 'telemetry', ['otel', 'kubernetes'], ['Инфраструктура', 'Надёжность']),
|
||||
theme(2024, 11, 'deprecation', 'Разработка', 'удаление устаревшего', 'вывести старый API или модуль без внезапного обрыва для потребителей', 'депрекация включает инвентаризацию, предупреждение, миграционный путь и конечную дату', 'старый endpoint никто не поддерживает, но неизвестно, кто им ещё пользуется', 'writing', ['mdnFetch', 'kubernetes'], ['Рефакторинг', 'API']),
|
||||
theme(2024, 12, 'maintenance-retro', 'Сопровождение', 'год сопровождения системы', 'собрать повторяющиеся проблемы в план улучшений вместо списка раздражителей', 'сопровождение — это данные об инцидентах, времени изменений и качестве обратной связи', 'команда много работает, но каждый квартал исправляет одну и ту же категорию ошибок', 'writing', ['otel', 'playwright'], ['Сопровождение', 'Команда']),
|
||||
|
||||
theme(2025, 1, 'ai-coding-assistant', 'AI и разработка', 'помощник для написания кода', 'использовать генерацию как ускоритель черновика, не передавая ей ответственность за решение', 'модель может предложить форму кода, но требования, тест и проверка остаются задачей инженера', 'сгенерированный фрагмент выглядит убедительно, но не учитывает границы конкретного API', 'ai', ['nistAi', 'playwright'], ['AI', 'Разработка']),
|
||||
theme(2025, 2, 'ai-code-verification', 'AI и разработка', 'проверка сгенерированного кода', 'строить тесты и ревью вокруг риска, а не вокруг доверия к красивому ответу', 'проверка включает контракт, негативные сценарии, безопасность и воспроизводимый запуск', 'ассистент сгенерировал исправление, которое проходит happy path, но ломает авторизацию', 'ai', ['nistAi', 'playwright', 'owaspHtml'], ['AI', 'Качество']),
|
||||
theme(2025, 3, 'knowledge-retrieval', 'Знания', 'поиск по инженерной базе знаний', 'сделать источник и дату важнее уверенного пересказа', 'retrieval полезен только при явном корпусе, ссылке на источник и механизме обновления знаний', 'ответ на вопрос выглядит правдоподобно, но ссылается на устаревший документ', 'ai', ['nistAi'], ['AI', 'Документация']),
|
||||
theme(2025, 4, 'ai-data-privacy', 'AI и безопасность', 'данные и приватность в AI-инструментах', 'до отправки контекста понять, какие данные можно передавать внешнему сервису', 'данные, права доступа, срок хранения и журналирование должны быть частью выбора инструмента', 'в prompt попал фрагмент production-лога с персональными данными', 'ai', ['nistAi', 'owaspHtml'], ['AI', 'Безопасность']),
|
||||
theme(2025, 5, 'engineering-automation', 'Автоматизация', 'автоматизация рутины', 'выбрать процесс, который повторяется достаточно часто и имеет проверяемый результат', 'автоматизация безопасна, когда у неё есть вход, выход, идемпотентность и возможность отмены', 'скрипт быстро закрывает заявки, но иногда удаляет нужное поле из формы', 'ops', ['docker', 'playwright'], ['Автоматизация', 'Разработка']),
|
||||
theme(2025, 6, 'developer-experience', 'Developer Experience', 'удобство внутреннего инструмента', 'измерять путь разработчика от клона репозитория до первого полезного изменения', 'DX складывается из документации, скорости обратной связи, локальной среды и понятных ошибок', 'новичок тратит два дня на запуск проекта, хотя сама задача занимает час', 'writing', ['docker', 'playwright'], ['DX', 'Команда']),
|
||||
theme(2025, 7, 'product-metrics', 'Продукт', 'метрики продукта для инженера', 'использовать метрику как сигнал поведения, а не как повод оптимизировать цифру в вакууме', 'метрика нужна вместе с сегментом, периодом, ограничениями и возможной побочной ценой', 'конверсия выросла, но поддержка получила вдвое больше обращений', 'telemetry', ['otel', 'mdnPerformance'], ['Продукт', 'Аналитика']),
|
||||
theme(2025, 8, 'tool-ux-research', 'Инструменты', 'исследование UX внутреннего инструмента', 'наблюдать реальную работу коллег вместо предположения, где им неудобно', 'внутренний интерфейс тоже продукт: у него есть пользователи, сценарии, ошибки и стоимость времени', 'команда просит «сделать кнопку удобнее», но никто не видел, как они обходят её сейчас', 'writing', ['wcag', 'mdnPerformance'], ['Инструменты', 'UX']),
|
||||
theme(2025, 9, 'engineering-interviews', 'Команда', 'инженерное интервью', 'собирать опыт через конкретный контекст, решение и последствия, а не через абстрактный совет', 'хорошее интервью раскрывает ход мысли и ограничения, поэтому полезно другим инженерам', 'эксперт рассказывает о «лучшем подходе», но не упоминает, при каких условиях он не работает', 'writing', ['dGc', 'otel'], ['Команда', 'Интервью']),
|
||||
theme(2025, 10, 'teaching-engineering', 'Обучение', 'объяснение сложной темы', 'вести читателя от наблюдаемого симптома к модели и самостоятельной проверке', 'обучающая заметка ценна, когда человек может повторить результат и понять, почему он получился', 'после статьи читатель копирует команду, но не знает, что делать при другой ошибке', 'writing', ['playwright', 'mdnPerformance'], ['Обучение', 'Разработка']),
|
||||
theme(2025, 11, 'research-method', 'Исследование', 'проверка источников', 'разделять официальную документацию, наблюдение в проекте и личную гипотезу', 'дата, версия и контекст источника меняют смысл даже технически верного совета', 'популярный ответ из поиска повторяют годами, хотя API давно поменялся', 'writing', ['mdnModules', 'nistAi'], ['Исследование', 'Документация']),
|
||||
theme(2025, 12, 'year-synthesis', 'Развитие', 'синтез инженерного года', 'выделить несколько устойчивых принципов из десятков частных задач', 'синтез не заменяет детали, но показывает повторяющийся механизм за разными инцидентами', 'статьи за год выглядят разрозненными, хотя все они говорят о границах и обратной связи', 'writing', ['otel', 'nistAi'], ['Развитие', 'Автор']),
|
||||
|
||||
theme(2026, 1, 'platform-api', 'Платформа', 'API платформенной команды', 'сделать внутренний сервис удобным для потребителя и управляемым для владельца', 'платформенный API — это контракт, документация, поддержка изменений и измеряемый уровень сервиса', 'команды начинают обходить внутренний сервис напрямую через базу данных', 'platform', ['kubernetes', 'otel'], ['Платформа', 'API']),
|
||||
theme(2026, 2, 'resilience', 'Надёжность', 'устойчивость к отказам', 'проектировать ожидаемые сбои зависимостей как штатный сценарий', 'timeout, retry, limit и fallback работают только вместе с наблюдением и ограничением нагрузки', 'внешний API отвечает медленно, и очередь запросов начинает валить собственное приложение', 'telemetry', ['otel', 'mdnFetch'], ['Надёжность', 'Backend']),
|
||||
theme(2026, 3, 'data-contracts', 'Данные', 'контракты данных', 'зафиксировать смысл полей, владельца и правила совместимости между системами', 'схема без семантики не защищает от неверной трактовки значения и времени его актуальности', 'поле status одинаково называется в двух сервисах, но означает разные стадии процесса', 'writing', ['postgres', 'mdnFetch'], ['Данные', 'Архитектура']),
|
||||
theme(2026, 4, 'modern-web-security', 'Безопасность', 'современная безопасность веба', 'соединить защиту браузера, API, сессии и цепочки доставки в одну модель', 'угроза проходит через границы: ввод, браузер, сеть, сервис, зависимость и оператор', 'исправление CORS не решило проблему, потому что endpoint всё ещё принимает чужой POST с cookie', 'security', ['owaspCsrf', 'owaspHtml', 'owaspUpload'], ['Безопасность', 'Web']),
|
||||
theme(2026, 5, 'systems-performance', 'Производительность', 'производительность системы', 'искать узкое место по измерению, а не оптимизировать самый знакомый компонент', 'задержка запроса складывается из очереди, сети, CPU, диска, базы и внешних вызовов', 'график CPU низкий, но пользователь всё равно ждёт страницу десять секунд', 'telemetry', ['otel', 'postgres', 'mdnPerformance'], ['Производительность', 'Надёжность']),
|
||||
theme(2026, 6, 'multi-runtime', 'Разработка', 'PHP, JavaScript и D в одном ландшафте', 'выбирать язык по границе задачи, а не пытаться сделать один стек универсальным', 'разные runtime полезны, если контракты, наблюдение и владение системой остаются едиными', 'часть обработки на D работает быстро, но её лог невозможно связать с PHP-запросом', 'd', ['dGc', 'otel', 'mdnModules'], ['PHP', 'JavaScript', 'DLang']),
|
||||
theme(2026, 7, 'migration-playbook', 'Миграции', 'план миграции', 'сделать переход обратимым и контролируемым через этапы и сигналы', 'миграция безопасна, когда старая и новая схема могут сосуществовать, а переключение измеримо', 'перенос авторизации уже начался, но команда не знает, как откатиться при сбое', 'writing', ['kubernetes', 'postgres'], ['Миграции', 'Архитектура']),
|
||||
theme(2026, 8, 'end-to-end-observability', 'Наблюдаемость', 'end-to-end наблюдаемость', 'связать пользовательское действие, браузерный тест, API и инфраструктурный trace', 'сигналы полезны, когда общий идентификатор сохраняется через все границы системы', 'e2e-тест упал, но невозможно быстро найти соответствующий backend-запрос', 'telemetry', ['otel', 'playwright'], ['Наблюдаемость', 'Тестирование']),
|
||||
theme(2026, 9, 'frontend-backend-boundary', 'Архитектура', 'граница frontend и backend', 'разделить представление, контракт и бизнес-правила без дублирования логики', 'граница определяется ответственностью за данные и решение, а не только HTTP-вызовом', 'правило скидки частично живёт в JavaScript, частично в PHP и расходится в редком сценарии', 'http', ['mdnFetch', 'postgres'], ['Frontend', 'Backend']),
|
||||
theme(2026, 10, 'code-review-standard', 'Качество', 'стандарт code review', 'сделать ревью способом снизить риск и передать контекст, а не соревнованием в замечаниях', 'хорошее ревью проверяет цель изменения, границы, тесты, наблюдение и будущую поддержку', 'в PR много комментариев по форматированию, но никто не заметил необратимую миграцию', 'writing', ['playwright', 'otel'], ['Качество', 'Команда']),
|
||||
theme(2026, 11, 'technology-evaluation', 'Архитектура', 'сравнение технологий без хайпа', 'выбирать инструмент по задаче, зрелости команды, ограничениям и стоимости выхода', 'технология не бывает лучшей вообще: у неё есть сильные стороны, риски и цена сопровождения', 'команда хочет заменить рабочую очередь, потому что увидела новый инструмент на конференции', 'writing', ['redis', 'kubernetes', 'dGc'], ['Архитектура', 'Инструменты']),
|
||||
theme(2026, 12, 'portfolio-case', 'Развитие', 'инженерный кейс от проблемы до результата', 'показать не только красивый финал, но и путь измерения, решения и последствий', 'хороший кейс сохраняет исходные условия и критерии успеха, иначе его нельзя перенести в другой контекст', 'в презентации есть новая архитектура, но нет данных, какую проблему она реально решила', 'writing', ['otel', 'mdnPerformance'], ['Развитие', 'Автор']),
|
||||
|
||||
theme(2027, 1, 'debugging-decade', 'Практика', 'десять лет web-диагностики', 'собрать общий алгоритм поиска причины от симптома до проверяемой гипотезы', 'диагностика движется по слоям: пользовательский эффект, запрос, приложение, зависимость, среда', 'после многих часов правки кода выясняется, что проблема была в конфигурации прокси', 'writing', ['otel', 'mdnFetch'], ['Диагностика', 'Развитие']),
|
||||
theme(2027, 2, 'bitrix-lessons', 'Bitrix', 'уроки Bitrix для legacy-разработки', 'превратить опыт работы с большой CMS в общие принципы сопровождения зрелых систем', 'ценность legacy-системы в её реальных правилах и интеграциях, которые нельзя увидеть по одной папке исходников', 'новая команда хочет всё переписать, не составив карту событий, инфоблоков и пользовательских сценариев', 'bitrix', ['bitrix', 'postgres'], ['Bitrix', 'Legacy']),
|
||||
theme(2027, 3, 'd-lessons', 'D', 'уроки D для прикладного инженера', 'перенести привычку измерять память, типы и время выполнения в любой язык', 'системное мышление полезно даже в высокоуровневом стеке: оно задаёт вопросы о цене абстракции', 'проблема производительности объясняется не языком, а количеством скрытых выделений и копий данных', 'd', ['dGc', 'dMemory'], ['DLang', 'Производительность']),
|
||||
theme(2027, 4, 'build-evolution', 'Сборка', 'эволюция frontend-сборки', 'отделить полезные идеи модульности и кеширования от конкретной моды на инструмент', 'сборка существует, чтобы создать предсказуемый артефакт с понятным графом зависимостей', 'новая сборка быстрее, но команда не умеет диагностировать, почему в bundle попал лишний модуль', 'js', ['webpack', 'mdnModules'], ['JavaScript', 'Сборка']),
|
||||
theme(2027, 5, 'http-tls-guide', 'HTTP', 'полевой справочник HTTP и TLS', 'связать статусы, кеш, таймауты, сертификаты и поведение клиента в одной модели', 'протокол становится понятным, когда каждый симптом сопоставлен с уровнем, который его создаёт', 'ошибку 504 пытаются лечить браузером, хотя причина находится в upstream-сервисе', 'http', ['mdnCache', 'mdnFetch', 'owaspHtml'], ['HTTP', 'SSL']),
|
||||
theme(2027, 6, 'performance-capstone', 'Производительность', 'большой разбор производительности', 'показать путь от жалобы пользователя до ограничения, которое реально изменило время ответа', 'производительность — это последовательность измерений и гипотез, а не коллекция популярных оптимизаций', 'команда уменьшила bundle, а задержка не изменилась из-за медленного SQL-запроса', 'telemetry', ['mdnPerformance', 'postgres', 'otel'], ['Производительность', 'Кейс']),
|
||||
theme(2027, 7, 'reliability-capstone', 'Надёжность', 'большой разбор надёжности', 'собрать timeout, retry, очередь, мониторинг и восстановление в один сценарий', 'надёжность появляется из контролируемого поведения при сбоях, а не из надежды на отсутствие сбоев', 'при недоступности зависимости сервис создаёт лавину повторных запросов и ухудшает ситуацию', 'telemetry', ['otel', 'redis', 'kubernetes'], ['Надёжность', 'Кейс']),
|
||||
theme(2027, 8, 'security-capstone', 'Безопасность', 'большой разбор безопасности', 'показать защиту как последовательность решений от формы до production-окружения', 'безопасность требует слоёв: проверка ввода, права, сессия, журналирование и безопасная доставка', 'одно исправление XSS не помогло, потому что загруженные файлы всё ещё доступны из webroot', 'security', ['owaspUpload', 'owaspCsrf', 'owaspHtml'], ['Безопасность', 'Кейс']),
|
||||
theme(2027, 9, 'mentor-series', 'Наставничество', 'серия для инженера, который растёт', 'дать человеку не список технологий, а способ ставить вопросы и проверять выводы', 'рост инженера ускоряется через малые эксперименты, обратную связь и письменную фиксацию знания', 'junior копирует решения, но не понимает, по каким признакам выбирать следующий шаг', 'writing', ['playwright', 'mdnPerformance'], ['Наставничество', 'Развитие']),
|
||||
theme(2027, 10, 'long-form-interview', 'Интервью', 'длинное техническое интервью', 'раскрыть решение через реальные ограничения, а не через набор победных формулировок', 'история технологии становится полезной, когда в ней видны сомнения, отказы и критерии выбора', 'спикер рассказывает о микросервисах, но интереснее узнать, почему команда не пошла этим путём раньше', 'writing', ['dGc', 'otel'], ['Интервью', 'Разработка']),
|
||||
theme(2027, 11, 'mistakes-revisions', 'Ретроспектива', 'пересмотр старых советов', 'исправлять собственные публикации и показывать, что новое знание меняет практику', 'инженерная зрелость проявляется не в безошибочности, а в способности обновить вывод при новых данных', 'старый совет всё ещё популярен, хотя за годы изменились браузеры, инструменты и угрозы', 'writing', ['mdnModules', 'owaspHtml'], ['Ретроспектива', 'Автор']),
|
||||
theme(2027, 12, 'author-manifesto', 'Автор', 'манифест инженерного письма', 'зафиксировать принципы блога: личный опыт, проверка, ограничения и уважение к читателю', 'техническая статья соединяет факт, модель, действие и честное описание неопределённости', 'после десяти лет публикаций нужно решить, какие темы больше не стоит писать без собственного опыта', 'writing', ['nistAi', 'otel'], ['Автор', 'Развитие']),
|
||||
];
|
||||
|
||||
const covers = {
|
||||
bitrix: '/assets/illustrations/bitrix-photo-editor.svg',
|
||||
php: '/assets/illustrations/php-ssl-cacert.svg',
|
||||
js: '/assets/illustrations/jquery-webpack.svg',
|
||||
http: '/assets/illustrations/firefox-timeout.svg',
|
||||
docker: '/assets/illustrations/vibe-cover.svg',
|
||||
ops: '/assets/illustrations/tilix-cover.svg',
|
||||
sql: '/assets/illustrations/vibe-db.svg',
|
||||
redis: '/assets/illustrations/vibe-async.svg',
|
||||
d: '/assets/illustrations/dconf-gc-cover.svg',
|
||||
frontend: '/assets/illustrations/vibe-web.svg',
|
||||
security: '/assets/illustrations/cover-virus.svg',
|
||||
test: '/assets/illustrations/tilix-screenshot.svg',
|
||||
telemetry: '/assets/illustrations/gc-small-pools.svg',
|
||||
platform: '/assets/illustrations/vibe-api.svg',
|
||||
ai: '/assets/illustrations/ctfe-engine.svg',
|
||||
writing: '/assets/illustrations/tilix-cover.svg',
|
||||
};
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function paragraph(text) {
|
||||
return '<p>' + text + '</p>';
|
||||
}
|
||||
|
||||
function heading(text) {
|
||||
return '<h2>' + text + '</h2>';
|
||||
}
|
||||
|
||||
function code(text) {
|
||||
return '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||||
}
|
||||
|
||||
function list(items, ordered) {
|
||||
const tag = ordered ? 'ol' : 'ul';
|
||||
return '<' + tag + '>' + items.map((item) => '<li>' + item + '</li>').join('') + '</' + tag + '>';
|
||||
}
|
||||
|
||||
function sourceList(keys) {
|
||||
return list(keys.map((key) => {
|
||||
const source = sources[key];
|
||||
return '<a href="' + source.url + '" target="_blank" rel="noopener">' + source.label + '</a>';
|
||||
}), false);
|
||||
}
|
||||
|
||||
function voiceFor(year) {
|
||||
if (year <= 2018) {
|
||||
return {
|
||||
practice: 'Периодически в проекте встречается задача, которая с виду кажется мелкой, а потом съедает полдня. В этот раз разбираюсь с темой',
|
||||
mechanism: 'Сначала хотелось просто применить готовый рецепт, но без понимания механизма он быстро превращается в набор случайных действий. Поэтому давайте разберём',
|
||||
field: 'На реальном проекте эта история обычно начинается спокойно, а затем всплывает один неприятный крайний случай. Разберём',
|
||||
ending: 'Если держать этот порядок, решение остаётся понятным и через несколько месяцев.',
|
||||
};
|
||||
}
|
||||
if (year <= 2020) {
|
||||
return {
|
||||
practice: 'В рабочем коде важен не только результат на локальной машине, но и возможность повторить его в другой среде. Рассмотрим',
|
||||
mechanism: 'Перед настройкой полезно договориться о модели происходящего. Разберём',
|
||||
field: 'Разбор подобных случаев удобнее строить не вокруг виноватого компонента, а вокруг цепочки наблюдаемых фактов. Тема:',
|
||||
ending: 'Так задача перестаёт быть одноразовым исправлением и становится проверяемым процессом.',
|
||||
};
|
||||
}
|
||||
if (year <= 2023) {
|
||||
return {
|
||||
practice: 'Когда система растёт, локальная правка начинает затрагивать данные, сеть и ожидания пользователей. Ниже — практическая схема для темы',
|
||||
mechanism: 'Полезная инженерная модель должна объяснять не только нормальную работу, но и отказ. Рассмотрим',
|
||||
field: 'Начинаю такой разбор с фактов, временной шкалы и границ ответственности. Сегодняшний случай:',
|
||||
ending: 'Это не гарантирует отсутствие ошибок, но делает их обнаруживаемыми и управляемыми.',
|
||||
};
|
||||
}
|
||||
if (year <= 2025) {
|
||||
return {
|
||||
practice: 'Сопровождаемая система выигрывает не от самой модной технологии, а от ясной границы и понятной проверки. Разберём',
|
||||
mechanism: 'За названием инструмента обычно скрывается договор о данных, времени и ответственности. В теме',
|
||||
field: 'В зрелом проекте полезнее описывать не один удачный ход, а контекст, альтернативы и последствия. Кейс:',
|
||||
ending: 'Зафиксированный контракт экономит команде больше времени, чем ещё один удачный хак.',
|
||||
};
|
||||
}
|
||||
if (year === 2026) {
|
||||
return {
|
||||
practice: 'На стыке frontend, backend и платформы решение нужно оценивать как часть целой системы. Практический разбор темы',
|
||||
mechanism: 'Системное объяснение должно связывать локальный код с тем, что увидит пользователь и оператор. Разбираем',
|
||||
field: 'Ниже не универсальный рецепт, а воспроизводимый способ исследовать решение с его ограничениями. Тема:',
|
||||
ending: 'Именно так отдельная практика превращается в переносимый инженерный метод.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
practice: 'После десяти лет заметок я всё чаще начинаю не с инструмента, а с наблюдаемого эффекта и способа его измерить. Тема этой практики:',
|
||||
mechanism: 'Зрелая техническая заметка должна оставить читателю не только команду, но и модель для следующей незнакомой ситуации. Разберём',
|
||||
field: 'Этот разбор собирает повторяющийся паттерн из многих проектов, не выдавая его за универсальную истину. Тема:',
|
||||
ending: 'Пусть это будет не финальный ответ, а хороший вопрос и проверяемый следующий шаг.',
|
||||
};
|
||||
}
|
||||
|
||||
function capitalize(value) {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function titleFor(topic, kind) {
|
||||
const subject = capitalize(topic.subject);
|
||||
if (topic.year <= 2018) {
|
||||
if (kind === 'practice') return topic.area + '. ' + subject + ': рабочая схема';
|
||||
if (kind === 'mechanism') return topic.area + '. Почему важна тема: ' + subject;
|
||||
return topic.area + '. ' + subject + ': разбор типичной ошибки';
|
||||
}
|
||||
if (topic.year <= 2021) {
|
||||
if (kind === 'practice') return topic.area + '. Как работать с темой: ' + subject;
|
||||
if (kind === 'mechanism') return 'Под капотом: ' + subject;
|
||||
return 'Разбор: ' + subject + ' в реальном проекте';
|
||||
}
|
||||
if (topic.year <= 2024) {
|
||||
if (kind === 'practice') return subject + ': минимальная инженерная схема';
|
||||
if (kind === 'mechanism') return subject + ': модель, ограничения и границы';
|
||||
return subject + ': диагностика, решение и проверка';
|
||||
}
|
||||
if (kind === 'practice') return subject + ': практический маршрут';
|
||||
if (kind === 'mechanism') return subject + ': как принять инженерное решение';
|
||||
return subject + ': кейс с ограничениями и выводами';
|
||||
}
|
||||
|
||||
function excerptFor(topic, kind) {
|
||||
if (kind === 'practice') {
|
||||
return 'Практическая заметка о том, как ' + topic.problem + '. С минимальной схемой, проверкой результата и ограничениями.';
|
||||
}
|
||||
if (kind === 'mechanism') {
|
||||
return 'Разбираем, почему ' + topic.model + ' — и какие ошибки возникают, если этот механизм не учитывать.';
|
||||
}
|
||||
return 'Кейс о том, как ' + topic.field + '. В конце — последовательность проверки и критерий готовности.';
|
||||
}
|
||||
|
||||
function codeFor(family, kind) {
|
||||
const variants = {
|
||||
bitrix: {
|
||||
practice: [
|
||||
"CModule::IncludeModule('iblock');",
|
||||
'$element = new CIBlockElement();',
|
||||
'$id = $element->Add([',
|
||||
" 'IBLOCK_ID' => 12,",
|
||||
" 'NAME' => $name,",
|
||||
" 'ACTIVE' => 'Y',",
|
||||
" 'PROPERTY_VALUES' => $properties,",
|
||||
']);',
|
||||
'if (!$id) {',
|
||||
" throw new RuntimeException($element->LAST_ERROR);",
|
||||
'}',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'$id = $element->Add($fields);',
|
||||
'if ($id === false) {',
|
||||
" error_log('Bitrix error: ' . $element->LAST_ERROR);",
|
||||
' return null;',
|
||||
'}',
|
||||
'return (int) $id;',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'$required = [\'IBLOCK_ID\', \'NAME\'];',
|
||||
'foreach ($required as $field) {',
|
||||
' if (empty($fields[$field])) {',
|
||||
" throw new InvalidArgumentException($field . ' is required');",
|
||||
' }',
|
||||
'}',
|
||||
].join('\n'),
|
||||
},
|
||||
php: {
|
||||
practice: [
|
||||
"if (($_FILES['image']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {",
|
||||
" throw new RuntimeException('Upload failed');",
|
||||
'}',
|
||||
"$tmp = $_FILES['image']['tmp_name'];",
|
||||
"$target = $storage . '/' . bin2hex(random_bytes(12)) . '.jpg';",
|
||||
'if (!move_uploaded_file($tmp, $target)) {',
|
||||
" throw new RuntimeException('Cannot move upload');",
|
||||
'}',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'set_error_handler(function ($severity, $message, $file, $line) {',
|
||||
" error_log(json_encode(['severity' => $severity, 'message' => $message, 'file' => $file, 'line' => $line]));",
|
||||
' return false;',
|
||||
'});',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'$context = [',
|
||||
" 'request_id' => $requestId,",
|
||||
" 'operation' => 'external_api_call',",
|
||||
" 'status' => $response->getStatusCode(),",
|
||||
'];',
|
||||
'error_log(json_encode($context));',
|
||||
].join('\n'),
|
||||
},
|
||||
js: {
|
||||
practice: [
|
||||
"import { mountForm } from './form.js';",
|
||||
'',
|
||||
"const root = document.querySelector('[data-form]');",
|
||||
'if (root) {',
|
||||
' mountForm(root);',
|
||||
'}',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'async function loadData(url) {',
|
||||
' const response = await fetch(url);',
|
||||
' if (!response.ok) throw new Error(String(response.status));',
|
||||
' return response.json();',
|
||||
'}',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'button.addEventListener(\'click\', async () => {',
|
||||
' button.disabled = true;',
|
||||
' try {',
|
||||
' await save();',
|
||||
' } finally {',
|
||||
' button.disabled = false;',
|
||||
' }',
|
||||
'});',
|
||||
].join('\n'),
|
||||
},
|
||||
http: {
|
||||
practice: [
|
||||
'curl -I https://example.test/api/items',
|
||||
'',
|
||||
'# Смотрим не только статус:',
|
||||
'# Cache-Control, ETag, Via, Server,',
|
||||
'# время ответа и цепочку прокси.',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'fetch(url, {',
|
||||
" cache: 'no-cache',",
|
||||
" headers: { 'X-Request-ID': requestId },",
|
||||
'}).then((response) => {',
|
||||
' if (!response.ok) throw new Error(String(response.status));',
|
||||
' return response.json();',
|
||||
'});',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'client timeout < proxy timeout < worker timeout',
|
||||
'',
|
||||
'Для каждого уровня:',
|
||||
'1. фиксируем значение;',
|
||||
'2. пишем владельца;',
|
||||
'3. проверяем поведение при обрыве.',
|
||||
].join('\n'),
|
||||
},
|
||||
docker: {
|
||||
practice: [
|
||||
'FROM php:8.3-cli',
|
||||
'WORKDIR /app',
|
||||
'COPY composer.json composer.lock ./',
|
||||
'RUN composer install --no-dev --prefer-dist',
|
||||
'COPY . .',
|
||||
'CMD ["php", "public/index.php"]',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'services:',
|
||||
' app:',
|
||||
' build: .',
|
||||
' environment:',
|
||||
' APP_ENV: development',
|
||||
' ports:',
|
||||
' - "8080:8080"',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'docker compose config',
|
||||
'docker compose up --build',
|
||||
'docker compose logs --tail=100 app',
|
||||
'docker compose exec app php -v',
|
||||
].join('\n'),
|
||||
},
|
||||
ops: {
|
||||
practice: [
|
||||
'APP_ENV=development',
|
||||
'APP_DEBUG=0',
|
||||
'DATABASE_URL=...',
|
||||
'',
|
||||
'# Секреты не добавляем в git и не выводим в журнал.',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'const required = [\'DATABASE_URL\', \'APP_ENV\'];',
|
||||
'for (const name of required) {',
|
||||
' if (!process.env[name]) throw new Error(name + \' is required\');',
|
||||
'}',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'1. Проверить версию runtime.',
|
||||
'2. Проверить конфигурацию без секретов.',
|
||||
'3. Проверить сетевую доступность зависимости.',
|
||||
'4. Зафиксировать результат в runbook.',
|
||||
].join('\n'),
|
||||
},
|
||||
sql: {
|
||||
practice: [
|
||||
'EXPLAIN (ANALYZE, BUFFERS)',
|
||||
'SELECT id, name',
|
||||
'FROM products',
|
||||
'WHERE category_id = $1',
|
||||
'ORDER BY updated_at DESC',
|
||||
'LIMIT 20;',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'BEGIN;',
|
||||
'UPDATE stock SET amount = amount - 1 WHERE product_id = $1;',
|
||||
'INSERT INTO orders (product_id, user_id) VALUES ($1, $2);',
|
||||
'COMMIT;',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'CREATE INDEX CONCURRENTLY products_category_updated_idx',
|
||||
'ON products (category_id, updated_at DESC);',
|
||||
'',
|
||||
'-- После этого снова смотрим EXPLAIN ANALYZE.',
|
||||
].join('\n'),
|
||||
},
|
||||
redis: {
|
||||
practice: [
|
||||
'XADD jobs * type export report_id 42',
|
||||
'XGROUP CREATE jobs exports $ MKSTREAM',
|
||||
'XREADGROUP GROUP exports worker-1 COUNT 1 BLOCK 5000 STREAMS jobs >',
|
||||
'XACK jobs exports 1700000000000-0',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'cache key: profile:42',
|
||||
'owner: profile service',
|
||||
'ttl: 5m',
|
||||
'invalidate on: ProfileUpdated',
|
||||
'fallback: read primary storage',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'1. Записать задачу с ключом идемпотентности.',
|
||||
'2. Взять задачу worker-ом.',
|
||||
'3. Сохранить результат.',
|
||||
'4. Подтвердить обработку только после успеха.',
|
||||
].join('\n'),
|
||||
},
|
||||
d: {
|
||||
practice: [
|
||||
'enum table = buildLookupTable();',
|
||||
'static assert(table.length == 256);',
|
||||
'',
|
||||
'void handleRequest() {',
|
||||
' // В горячем пути сначала измеряем аллокации.',
|
||||
'}',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'import core.memory : GC;',
|
||||
'',
|
||||
'GC.disable();',
|
||||
'scope(exit) GC.enable();',
|
||||
'',
|
||||
'// Использовать только в измеренном локальном участке.',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'dub test',
|
||||
'dub build --build=release',
|
||||
'# Затем измеряем latency, память и профиль аллокаций.',
|
||||
].join('\n'),
|
||||
},
|
||||
frontend: {
|
||||
practice: [
|
||||
'<label for="email">Почта</label>',
|
||||
'<input id="email" name="email" type="email" autocomplete="email">',
|
||||
'<button type="submit">Сохранить</button>',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'new PerformanceObserver((list) => {',
|
||||
' for (const entry of list.getEntries()) {',
|
||||
' console.log(entry.name, entry.duration);',
|
||||
' }',
|
||||
'}).observe({ type: \'navigation\', buffered: true });',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'1. Пройти сценарий с клавиатуры.',
|
||||
'2. Проверить состояние loading и error.',
|
||||
'3. Проверить медленную сеть.',
|
||||
'4. Записать метрику до и после изменения.',
|
||||
].join('\n'),
|
||||
},
|
||||
security: {
|
||||
practice: [
|
||||
'if (!sameOrigin(request) || !validCsrfToken(request)) {',
|
||||
' return response.status(403).end();',
|
||||
'}',
|
||||
'',
|
||||
'if (!user.can(\'profile:update\')) {',
|
||||
' return response.status(403).end();',
|
||||
'}',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'allow-list extension: jpg, png, webp',
|
||||
'validate content independently',
|
||||
'generate server filename',
|
||||
'store outside web root',
|
||||
'serve through authorized handler',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'Asset -> actor -> entry point -> control -> evidence',
|
||||
'',
|
||||
'Если для контроля нет доказательства, считаем его непроверенным.',
|
||||
].join('\n'),
|
||||
},
|
||||
test: {
|
||||
practice: [
|
||||
'import { expect, test } from \'@playwright/test\';',
|
||||
'',
|
||||
'test(\'user saves profile\', async ({ page }) => {',
|
||||
' await page.goto(\'/profile\');',
|
||||
' await page.getByLabel(\'Почта\').fill(\'user@example.test\');',
|
||||
' await page.getByRole(\'button\', { name: \'Сохранить\' }).click();',
|
||||
' await expect(page.getByRole(\'status\')).toHaveText(\'Сохранено\');',
|
||||
'});',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'use: {',
|
||||
" trace: 'on-first-retry',",
|
||||
' screenshot: \'only-on-failure\',',
|
||||
'}',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'npx playwright test --trace on-first-retry',
|
||||
'npx playwright show-report',
|
||||
'# В trace смотрим действия, DOM-снимки и сеть.',
|
||||
].join('\n'),
|
||||
},
|
||||
telemetry: {
|
||||
practice: [
|
||||
'request_id=8f2d operation=checkout status=202 duration_ms=42',
|
||||
'',
|
||||
'# Одно событие — одна строка с полями,',
|
||||
'# по которым его можно найти и агрегировать.',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'trace: путь запроса',
|
||||
'metric: число или распределение во времени',
|
||||
'log: контекст конкретного события',
|
||||
'',
|
||||
'Связываем их общим request_id или trace_id.',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'1. Видим ухудшение по метрике.',
|
||||
'2. Находим trace конкретного запроса.',
|
||||
'3. Читаем логи нужного span.',
|
||||
'4. Проверяем исправление тем же сигналом.',
|
||||
].join('\n'),
|
||||
},
|
||||
platform: {
|
||||
practice: [
|
||||
'apiVersion: apps/v1',
|
||||
'kind: Deployment',
|
||||
'metadata:',
|
||||
' name: app',
|
||||
'spec:',
|
||||
' replicas: 2',
|
||||
' template:',
|
||||
' spec:',
|
||||
' containers:',
|
||||
' - name: app',
|
||||
' image: registry.example/app:sha',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'kubectl apply -f deployment.yaml',
|
||||
'kubectl rollout status deployment/app --timeout=10m',
|
||||
'kubectl rollout history deployment/app',
|
||||
'kubectl rollout undo deployment/app',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'1. Указать владельца API.',
|
||||
'2. Зафиксировать SLO и документацию.',
|
||||
'3. Включить наблюдение до запуска.',
|
||||
'4. Продумать путь отката.',
|
||||
].join('\n'),
|
||||
},
|
||||
ai: {
|
||||
practice: [
|
||||
'task: "Измени функцию расчёта скидки"',
|
||||
'constraints: ["не менять публичный API", "добавить негативные тесты"]',
|
||||
'evidence: ["diff", "tests", "security review"]',
|
||||
'',
|
||||
'# Ответ модели — вход в ревью, не доказательство корректности.',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'source -> retrieval -> generated draft -> test -> human decision',
|
||||
'',
|
||||
'Для каждого шага нужны:',
|
||||
'владелец, ограничение данных и способ проверить результат.',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'1. Убрать секреты и персональные данные из контекста.',
|
||||
'2. Проверить утверждения по источнику.',
|
||||
'3. Запустить тесты и негативные сценарии.',
|
||||
'4. Сохранить решение и причину принятия.',
|
||||
].join('\n'),
|
||||
},
|
||||
writing: {
|
||||
practice: [
|
||||
'# Контекст',
|
||||
'# Наблюдаемый симптом',
|
||||
'# Гипотеза и проверка',
|
||||
'# Решение и ограничения',
|
||||
'# Как воспроизвести результат',
|
||||
].join('\n'),
|
||||
mechanism: [
|
||||
'Decision: ...',
|
||||
'Context: ...',
|
||||
'Alternatives: ...',
|
||||
'Consequences: ...',
|
||||
'Review date: ...',
|
||||
].join('\n'),
|
||||
field: [
|
||||
'Факт -> источник -> гипотеза -> проверка -> решение -> последствия',
|
||||
'',
|
||||
'Если пропустить один шаг, текст легко превращается в уверенное мнение.',
|
||||
].join('\n'),
|
||||
},
|
||||
};
|
||||
|
||||
return variants[family][kind];
|
||||
}
|
||||
|
||||
function checklistFor(topic, kind) {
|
||||
if (kind === 'practice') {
|
||||
return [
|
||||
'Сформулировать вход и ожидаемый результат: ' + topic.problem + '.',
|
||||
'Выполнить минимальный сценарий отдельно от остальной системы.',
|
||||
'Проверить отрицательный путь: ' + topic.field + '.',
|
||||
'Сохранить наблюдаемый результат в тесте, логе или коротком runbook.',
|
||||
];
|
||||
}
|
||||
if (kind === 'mechanism') {
|
||||
return [
|
||||
'Назвать границу, на которой действует механизм.',
|
||||
'Зафиксировать, что считается успехом и отказом.',
|
||||
'Проверить, какие данные или ресурсы остаются после ошибки.',
|
||||
'Добавить измерение, которое подтвердит вывод в следующем проекте.',
|
||||
];
|
||||
}
|
||||
return [
|
||||
'Собрать симптомы до изменения конфигурации или кода.',
|
||||
'Проверить гипотезу самым маленьким безопасным экспериментом.',
|
||||
'Исправить причину, а не только видимый эффект.',
|
||||
'Добавить защиту или наблюдение, чтобы случай не вернулся незаметно.',
|
||||
];
|
||||
}
|
||||
|
||||
function contentFor(topic, kind) {
|
||||
const voice = voiceFor(topic.year);
|
||||
const sourceSection = heading('Материалы для проверки') + sourceList(topic.sourceKeys);
|
||||
const checklist = checklistFor(topic, kind);
|
||||
const runtime = topic.year <= 2019 ? 'в рабочем проекте' : 'в production';
|
||||
|
||||
if (kind === 'practice') {
|
||||
return [
|
||||
paragraph(voice.practice + ' «' + topic.subject + '». Цель заметки — ' + topic.problem + '. Не будем начинать с большой переделки: сначала соберём минимальный сценарий, который можно показать коллеге и повторить на чистой среде.'),
|
||||
heading('Минимальная рабочая схема'),
|
||||
paragraph('Первое правило здесь простое: отделяем входные данные от побочного эффекта. До того как менять состояние системы, проверяем условия, назначаем понятный идентификатор операции и оставляем достаточно контекста для диагностики. ' + capitalize(topic.model) + '.'),
|
||||
code(codeFor(topic.family, 'practice')),
|
||||
heading('Что проверяем после запуска'),
|
||||
list(checklist, true),
|
||||
heading('Где чаще всего ошибаются'),
|
||||
paragraph('Опасность темы «' + topic.subject + '» не в сложном синтаксисе, а в неявных предположениях. ' + capitalize(topic.field) + '. Если решение зависит от версии среды, внешней системы или прав пользователя, это лучше проверить отдельным шагом и записать рядом с кодом.'),
|
||||
sourceSection,
|
||||
paragraph(voice.ending),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
if (kind === 'mechanism') {
|
||||
return [
|
||||
paragraph(voice.mechanism + ' «' + topic.subject + '». ' + capitalize(topic.model) + '. Когда этот слой остаётся невидимым, команда начинает лечить следствие: добавляет таймаут, глобальную переменную, второй кеш или ещё одну повторную попытку.'),
|
||||
heading('Модель происходящего'),
|
||||
paragraph('Для начала полезно назвать владельца состояния, момент изменения и границу, за которую действие не должно протекать незаметно. Тогда можно отличить нормальную задержку от отказа, локальную оптимизацию от нарушения контракта и временный обход от постоянного решения.'),
|
||||
code(codeFor(topic.family, 'mechanism')),
|
||||
heading('Как проверить модель на практике'),
|
||||
list(checklist, true),
|
||||
heading('Ограничения'),
|
||||
paragraph('У этой модели нет магической силы: ' + topic.field + '. Поэтому ' + runtime + ' нужно добавлять наблюдение, разумные лимиты и понятный путь отката. Чем дороже ошибка, тем важнее заранее проговорить этот случай.'),
|
||||
sourceSection,
|
||||
paragraph(voice.ending),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return [
|
||||
paragraph(voice.field + ' «' + topic.subject + '». Типичная ситуация выглядит так: ' + topic.field + '. В такой момент легко срочно поправить видимый симптом, но полезнее пройти короткое расследование и оставить после него защиту для следующего раза.'),
|
||||
heading('Последовательность разбора'),
|
||||
list(checklist, true),
|
||||
heading('Минимальное доказательство'),
|
||||
paragraph('Нам не нужна идеальная модель всей системы. Достаточно такого эксперимента, который отделяет одну гипотезу от другой: повторить запрос, сравнить входы, посмотреть контекст операции или воспроизвести проблему на отдельной записи. ' + capitalize(topic.problem) + '.'),
|
||||
code(codeFor(topic.family, 'field')),
|
||||
heading('Что меняется после исправления'),
|
||||
paragraph('Исправление считается законченным, когда новый путь проверяется автоматически или наблюдается по явному сигналу. Иначе «' + topic.subject + '» вернётся в следующем релизе под другим именем. ' + capitalize(topic.model) + '.'),
|
||||
sourceSection,
|
||||
paragraph(voice.ending),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function createArticle(topic, kind, day) {
|
||||
const month = pad(topic.month);
|
||||
const date = String(topic.year) + '-' + month + '-' + pad(day) + 'T10:00:00+00:00';
|
||||
return {
|
||||
slug: 'editorial-' + topic.year + '-' + month + '-' + kind + '-' + topic.id,
|
||||
title: titleFor(topic, kind),
|
||||
date,
|
||||
author: 'DarkRiDDeR',
|
||||
categories: topic.categories,
|
||||
cover: covers[topic.family],
|
||||
excerpt: excerptFor(topic, kind),
|
||||
contentHtml: contentFor(topic, kind),
|
||||
readingMinutes: kind === 'field' ? 4 : 3,
|
||||
};
|
||||
}
|
||||
|
||||
function validatePlan(generated, original) {
|
||||
if (themes.length !== 120) {
|
||||
throw new Error('Expected 120 monthly themes, got ' + themes.length);
|
||||
}
|
||||
if (generated.length !== 358) {
|
||||
throw new Error('Expected 358 new articles, got ' + generated.length);
|
||||
}
|
||||
|
||||
const slugs = new Set();
|
||||
for (const article of original.concat(generated)) {
|
||||
if (slugs.has(article.slug)) {
|
||||
throw new Error('Duplicate slug: ' + article.slug);
|
||||
}
|
||||
slugs.add(article.slug);
|
||||
}
|
||||
|
||||
for (const topic of themes) {
|
||||
const key = topic.year + '-' + pad(topic.month);
|
||||
const expected = 3;
|
||||
const actual = generated.filter((article) => article.date.startsWith(key)).length
|
||||
+ original.filter((article) => article.date.startsWith(key)).length;
|
||||
if (actual < expected) {
|
||||
throw new Error('Month ' + key + ' has only ' + actual + ' articles');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existing = JSON.parse(await readFile(articlesPath, 'utf8'));
|
||||
const original = existing.filter((article) => !article.slug.startsWith('editorial-'));
|
||||
const generated = [];
|
||||
|
||||
for (const item of themes) {
|
||||
const slotPrefix = String(item.year) + '-' + pad(item.month);
|
||||
for (let index = 0; index < articleKinds.length; index += 1) {
|
||||
const kind = articleKinds[index];
|
||||
if (existingSlots.has(slotPrefix + '-' + kind)) continue;
|
||||
generated.push(createArticle(item, kind, [7, 15, 25][index]));
|
||||
}
|
||||
}
|
||||
|
||||
validatePlan(generated, original);
|
||||
const archive = original.concat(generated).sort((left, right) => new Date(right.date) - new Date(left.date));
|
||||
await writeFile(articlesPath, JSON.stringify(archive, null, 2) + '\n');
|
||||
|
||||
console.log('Published ' + generated.length + ' planned articles.');
|
||||
console.log('Archive size: ' + archive.length + ' articles.');
|
||||
Reference in New Issue
Block a user