Дисциплина не масштабируется
Поздний вечер пятницы. На счётчике задача тридцать первая за неделю. В очереди мелочь: убрать заархивированные закладки из поискового индекса. Через час хочу спать.
Пишу однострочный промпт. Спецификация? На такую задачу спека ни к чему, тут одно действие. Агент берёт флаг is_deleted, чистит индекс, прогоняет тесты, всё зелёное. Задача закрыта.
Через два дня пользователь жалуется: ищет старую статью, в выдаче рядом с ней десятки заархивированных, которых там быть не должно.
Сажусь разбирать и упираюсь. В Сортуле у «удалённой» закладки три разных формы. Пользователь снёс её сам: ставится флаг is_deleted. Политика хранения её заархивировала через год: ставится дата в archived_at. Модерация её скрыла: ставится причина в hidden_by_mod. Каждый способ проставляет своё поле, потому что исторически они появились в разное время и решали разные задачи. Я об этом знал, в голове это держал, кому-то даже объяснял. В пятничную задачу не положил.
Агент выполнил то, что было в задаче: убрал записи с is_deleted. Два других пути продолжали жить в индексе. Девяносто минут разбора в воскресенье вечером плюс отдельная задача на полный ре-индекс.
Шаг пропустил не агент, а я.
В третьей статье серии я перечислил пять навыков нового рабочего режима. Декомпозиция, контекст, архитектурные границы, критерии приёмки, диагностика сбоев. На бумаге выглядит цельно. На практике в пятницу вечером эти пять навыков держатся, пока я в состоянии их применять. К пятому часу проверки они начинают проседать первыми.
Это не недостаток дисциплины. Это её естественный потолок. Любая система, которая держится на том, что один человек не пропустит шаг тридцать раз в неделю, рано или поздно ловит свою пятницу.
Вывод после нескольких таких пятниц у меня сложился простой. Раз личная дисциплина не масштабируется, нужна внешняя. Такая, которую нельзя молча обойти. Технически встроенная в момент входа задачи в работу и в момент её выхода из работы.
Об устройстве этого контура и пойдёт речь дальше.
Что такое SENAR
В прошлых трёх статьях я несколько раз ссылался на SENAR, но ни разу не остановился подробно. Дальше речь о его устройстве, поэтому пора это исправить.
SENAR, от английского Supervised Engineering & Normative AI Regulation, это открытая методология инженерной работы с ИИ-агентами при разработке. Лицензия CC BY-SA 4.0, полные тексты на senar.tech. Соавтор, Вадим Соглаев, с которым мы её и собрали.
Готовой методологии под 100% ИИ-разработки в индустрии не нашлось, поэтому пришлось писать по итогам собственной практики. За полтора года через тридцать с лишним проектов на разных языках, в разных предметных областях, прошла одна и та же закономерность: проблемы повторяются, решения повторяются. На каком-то проекте они начали оседать в одно место. SENAR это и есть это «одно место», доведённое до состояния, в котором по нему может работать не только автор.
Из чего методология состоит:
-
Восемь правил. Принципы работы с агентом: формализация задачи, явные границы изменений, обязательные негативные сценарии, ручная правка как сигнал, и ещё четыре. Правила — это «что должно быть», а не «как именно реализовать».
-
Два шлюза качества задачи. QG-0 на входе задачи: задача не может быть взята в работу, пока в ней не оформлена спецификация. QG-2 на выходе: задача не может быть закрыта, пока её результат не сверен с критериями приёмки и пока ручные правки не зарегистрированы. В полном стандарте SENAR шлюзов пять (QG-0..QG-4), но в этой статье разбираются только два, которые замыкают контур одной задачи.
-
Типизированная память проекта. Решения, тупики, исключения, договорённости и фоновый контекст хранятся отдельными записями разных типов, которые подаются в задачу по релевантности. Об устройстве памяти подробно в следующей статье.
-
Метрики. FPSR (доля задач, решённых с первой попытки), MIR (доля задач, в которых потребовалась ручная правка после агента), DER (Dead End Rate: доля тупиковых попыток/времени на тупики). Дополнительно я веду служебную метрику возвратов после закрытия задачи (ERR), чтобы видеть дефекты, уехавшие в прод. Метрики работают как сигналы и показывают, какая часть контура сейчас провисает.
Форма у методологии не из соображений «так положено». Она вытекает из природы исполнителя. Во второй статье я подробно разбирал, чем агент отличается от программиста: он не удерживает контекст между запусками, склонен к локальной оптимизации, добросовестно исполняет буквальную постановку, не задаёт вопроса там, где для человека он был бы естественным. Если процесс не учитывает этих свойств, он ломается каждый раз, когда сложность проекта переходит порог одной задачи. SENAR — это и есть описание процесса, которое такие свойства учитывает по построению.
Эта статья и следующая разбирают методологию на две половины. Здесь это контур одной задачи: ворота на входе и выходе плюс метрики, которые показывают, что в контуре провисает. В следующей это среда, в которой агент живёт между воротами: правила работы с контекстом, архитектурные границы, типизированная память проекта. Шестая статья закроет границы применимости и открытые вопросы, на которые у меня пока нет ответа.
Что закрывают ворота задачи
Прежде чем разбирать ворота по отдельности, стоит сказать, чем они вообще отличаются от остальных проверок в процессе разработки.
Тесты ловят функциональные расхождения с ожидаемым поведением. Линтеры ловят отклонения от стиля и структуры кода. Статический анализ ловит подозрительные конструкции. Ревью ловит то, что не пойманное ничем выше. Эти проверки уже встроены в индустриальную практику, и в SENAR никто их не отменяет.
Ворота задачи закрывают другое. На входе они не пускают агента работать над задачей, в которой нет минимально необходимой структуры: цели, критериев, негативных сценариев, границ изменений. На выходе они не пускают закрыть задачу, пока её результат не сверен с тем, что было обещано на входе. То есть тесты и линтеры проверяют сам код. Ворота проверяют постановку и приёмку, в которых сам код вообще ещё не появился или уже произведён.
Аналогия из обычного производства. Тесты и линтеры это контроль готовых деталей на конвейере. Ворота задачи это допуск чертежа на конвейер и приёмка партии после конвейера. Без чертежа конвейер просто простаивает, без приёмки партия едет в склад без подтверждения качества. С агентом ситуация не лучше, чем с конвейером, скорее наоборот: он умеет делать что-то даже из плохого чертежа, и в этом главная опасность.
Ещё важная деталь. В TAUSIK, фреймворке, который реализует SENAR на практике, ворота встроены технически. До прохождения QG-0 агенту просто не отдают задачу в работу, а после прохождения QG-2 задача физически переходит в статус закрытой и уезжает в журнал. Пропустить шаг можно, только переписав сам инструмент. Та самая защита от пятничной усталости, ради которой ворота и появились.
QG-0: вход. Что должно быть в спеке
В третьей статье я перечислил пять навыков нового рабочего режима. Два из них, декомпозиция и критерии приёмки, должны быть зафиксированы прямо в задаче. Из головы постановщика они в задачу сами не переходят. QG-0 это и есть точка, где они становятся обязательными: формат спецификации задаёт, какими бывают критерии и как именно сложить декомпозицию задачи в её спеке.
Минимальная спецификация для контура одной задачи в этой статье:
-
Цель. Одно предложение в продуктовой логике, со стороны пользователя или клиента. Хорошая формулировка: «дать пользователю возможность сменить отображаемое имя». Плохая: «добавить поле в таблицу
users»; вторая описывает уже реализацию, и агент будет оптимизировать именно её. -
Критерии приёмки. Перечисление того, по чему я подтвержу, что задача решена. Минимум один критерий, в практике обычно три-пять. Каждый критерий проверяемый, с однозначным исходом.
-
Негативные сценарии. Что должно произойти при неправильном вводе, пограничном случае, отсутствующих данных, конкурентном изменении. Минимум один негативный сценарий — это правило без исключений.
-
Границы изменений. Какие файлы, модули, сервисы агенту трогать можно и какие категорически нельзя. Не обязательно жёсткий список путей; иногда достаточно области ответственности.
-
Ссылка на архитектурный контекст. Какие границы и инварианты задачи я пересекаю и где они описаны. Об этом подробнее в следующей статье, здесь достаточно факта, что ссылка есть.
В полном SENAR Standard у QG-0 есть и дополнительные обязательные пункты: связь с требованием/историей, тип работы, а для задач риска ещё и явный критерий по безопасности. Здесь фокус уже: минимальный профиль, который закрывает контур конкретной задачи на практике.
Меньше нельзя. Без любого из пунктов агент в первой же строке начинает достраивать смысл. Если цель пропущена, он оптимизирует локально по формулировке запроса. Если критерии не описаны, закроет задачу в собственной логике достаточности. Если негативные сценарии не выписаны, напишет позитивный путь и остановится. А когда границы изменений не заданы, полезет наводить порядок там, где никто не просил, и я снова потрачу два часа на разбор. Все эти ситуации из реальных проектов, в первой статье серии я три из них приводил подробно.
Больше обычно бюрократия. Пять-семь пунктов спеки на типовую задачу, десять-двенадцать на сложную. Если спека начинает занимать страницу плотного текста, задача скорее всего недодекомпозирована и её надо резать.
QG-0 в первую очередь проверяет структурную полноту: есть ли все обязательные секции, не пустые ли они. В зрелых конфигурациях поверх этого добавляются и качественные требования к критериям (независимая проверяемость, обязательный негативный сценарий и другие). Содержательную правильность по-прежнему держит человек. Ворота не отменяют его роль, они снимают с него обязанность помнить про эти пять пунктов в одиннадцать вечера.
Маленькая деталь, которая в начале вызывала у меня сопротивление. Спецификация на «мелкую задачу» по правилам выглядит почти так же, как на крупную. Поправка в одну строку требует тех же пяти секций. На первой неделе казалось, что это перебор. Через месяц стало ясно: больше всего дефектов в прод приносят как раз мелкие задачи, потому что на них у меня и срывалась дисциплина. Тот пример с заархивированными закладками из начала статьи был такой, на одну строку.
Критерии приёмки и негативные сценарии
Из всех пунктов спецификации этот приходится защищать чаще всего. Поэтому отдельный раздел.
В обычной разработке критерий приёмки часто звучит как «работает», и этого хватает, потому что между постановщиком и исполнителем стоит общее понимание продукта, прошлый опыт и хотя бы один разговор, в котором двусмысленности проговариваются голосом. С агентом «работает» это пустая строка. Он закроет задачу, как только пройдёт первый позитивный сценарий, потому что всё остальное в формулировке отсутствует.
Поэтому SENAR требует переводить «работает» в проверяемое. Вместо «корректно сохранять закладку» спецификация перечисляет: при сохранении закладки с уже существующим адресом возвращается ошибка 409, при сохранении с пустым телом — 400, при превышении размера превью оно режется до восьми килобайт, при ошибке внешнего сервиса обогащения сохраняется минимальная запись с пометкой enrichment_pending. Каждый пункт можно проверить руками или тестом, у каждого есть однозначный исход.
Негативные сценарии — это критерии приёмки про то, чего не должно произойти. Они выделены в спеке отдельной обязательной секцией. Причина простая: чаще всего пропускаются именно они. Человек, выписывая критерии, естественным образом думает в позитивной логике: что должно произойти, чтобы задача была закрыта. Негативная логика, то есть что должно произойти, чтобы система не сломалась, требует отдельного шага. Если негативные сценарии не вынесены в отдельный обязательный пункт, они растворяются в общих критериях, и в спеке их попросту нет.
Один эпизод с Сортулы, который мне обошёлся дороже всего. Задача была про повторную отправку ссылки в ВК-бот. Если пользователь второй раз пересылает тот же пост, обновить дату последнего просмотра у уже существующей закладки, дубль не создавать. Позитивный сценарий очевидный, я его написал. Негативный сценарий, который я не написал: что если пользователь пересылает тот же пост, сидя под другим аккаунтом ВК, привязанным к тому же телефону.
Агент решил позитивный путь чисто. Идентификатор закладки собирался из адреса поста плюс идентификатора отправителя ВК, поэтому одна и та же ссылка от двух аккаунтов одного пользователя порождала две разные закладки. Обе появлялись в ленте, обе попадали в поисковый индекс. Пользователь видел дубли и не понимал, почему.
В правильно оформленной спеке негативный сценарий звучал бы так: «один и тот же адрес поста, отправленный с разных идентификаторов отправителя одним и тем же пользователем, не должен порождать второй закладки». Этот пункт прошёл бы через QG-0 как обязательный, агент сразу увидел бы его в задаче, и она закрылась бы корректно. Без него я снова потратил вечер на разбор и отдельную задачу на дедуп уже накопленных дублей.
С тех пор спецификация на любую задачу, в которой есть хоть какие-то данные на входе, проходит через простой вопрос: что должно произойти, если на вход придёт что-то неожиданное. Пустое значение, повторное обращение, конкурентный запрос, ответ внешнего сервиса с ошибкой, превышение лимита. Хотя бы один из этих сценариев почти всегда применим. Если ни один не применим, скорее всего, я просто пока не подумал.
QG-2: выход. Как нельзя закрыть задачу
Если QG-0 не пускает плохую постановку в работу, то QG-2 не пускает плохую приёмку в журнал. В этой статье разбираю три управленческие проверки на выходе задачи; технические обязательные критерии QG-2 (CI, тесты, статанализ, линтер, security scan) остаются обязательной частью полного стандарта.
-
Сверка с критериями приёмки. Каждый критерий, записанный на входе, должен иметь явное подтверждение: тест, который его проверяет, ручная проверка с пометкой о том, что выполнена, или ссылка на артефакт, в котором результат виден. Просто «всё работает, я посмотрел» не проходит. В TAUSIK это устроено так, что задача физически не закрывается, пока в её карточке не отмечено состояние по каждому критерию. Зелёный тест, ручная пометка, скриншот, ссылка на отчёт. Что-то должно быть напротив каждой строки.
-
Регистрация ручных правок. Если за время работы я открывал код и правил руками, каждая такая правка должна быть зафиксирована короткой записью: что изменил, почему. Эта запись попадает в память проекта и помечает задачу как требовавшую ручной правки. По таким пометкам потом считается MIR. Маленькая правка — короткая запись. Соблазн молча поправить и закрыть задачу никуда не девается, но теперь он стоит явного шага, и это меняет дело.
-
Обновление памяти проекта. Если в ходе задачи всплыло что-то новое: пограничный случай, который раньше не был описан, особенность инфраструктуры, неожиданное поведение библиотеки. Это должно быть записано в память типизированной заметкой. В третьей статье я приводил тип
gotchaкак пример: фоновая задача без явного маршрута садится в очередь по умолчанию, которую никто не слушает, об этом узнаёшь только когда что-нибудь отвалится в проде. Такие заметки появляются именно на QG-2, когда задача уже сделана и видно, какие пробелы в среде она вскрыла. -
Проверка не на бумаге. В TAUSIK шаг QG-2 встроен в инструмент: команда закрытия задачи запускает все три проверки и блокирует закрытие, если хотя бы одна не пройдена. Перед глазами оказывается список того, что осталось доделать, прежде чем задача уйдёт в журнал.
Зачем такая щепетильность: в третьей статье серии я уже разбирал, что ручная правка кода в этой модели плохой рефлекс. Сама правка может быть нужной; проблема в том, что без её фиксации агент в следующей задаче снова опирается на свой исходный вариант, как будто правки не было. Запись закрывает эту дыру и одновременно делает MIR честным.
Эффект, который это даёт, не очень заметен на первой неделе. Заметным он становится через месяц, когда журнал закрытых задач начинает выглядеть честнее. Меньше задач без подтверждённых критериев. Меньше задач с молча поправленным кодом. Память проекта толще на полезные заметки. И главное, меньше тех самых пятничных задач, которые в воскресенье возвращаются багом в проде.
Метрики: FPSR, MIR, DER и ERR операционно
Метрики в SENAR не самоцель. Они сигналы, по которым видно, какая часть контура у тебя сейчас провисает.
Три основные в контуре SENAR (FPSR, MIR, DER), плюс служебная ERR в моём рабочем журнале. Все цифры дальше я брал из собственного рабочего журнала, без независимой проверки; сравнивать «меня без SENAR» за тот же период не с чем, контрольной группы не существует. Подробнее об этой оговорке в конце раздела.
FPSR, доля задач, решённых с первой попытки. Считается по факту: задача закрылась без возвратов на доработку. На моих проектах на серверной части в знакомой предметной области она вырастала с примерно сорока процентов на ранних проектах до семидесяти пяти-восьмидесяти после того, как формализация задач стала привычкой. На задачах с интерфейсами и продуктовым ощущением она устойчиво ниже, тридцать-сорок процентов, и это уже не лечится ни спецификацией, ни воротами. Об этом отдельно ниже.
О чём говорит низкая FPSR: чаще всего о пробелах в спецификации: задачи берутся в работу с пустыми пунктами, агент достраивает смысл, постановщик возвращает на доработку. Чинится через доводку QG-0: посмотреть, какие пункты спеки чаще всего пустые в задачах, которые потом возвращаются.
MIR, доля задач, в которых потребовалась ручная правка после работы агента. Считается по факту, благодаря обязательной фиксации правок на QG-2. В первый месяц на Сортуле MIR был около двадцати процентов. Через месяц, после того как накопился архитектурный контекст и спецификации стали детальнее, MIR опустился до пяти-семи процентов. Это число не цель: совсем нулевой MIR означает либо враньё в записях, либо работу только с тривиальными задачами, эту оговорку я подробно разбирал во второй статье.
Что показывает высокий MIR: контекст, который получает агент, неполный или неточный. Где-то систематически не хватает архитектурной информации, где-то спецификация задаёт результат, который не стыкуется с уже существующим кодом, где-то в задаче пропущены ограничения. Лекарство одно: разобрать, какие правки повторяются, и закрыть их источник на уровне контекста или спецификации.
DER (Dead End Rate), доля тупиковых попыток в работе над задачами. В операционном журнале считаю его в двух видах: время на тупики к общему времени задачи и количество тупиков на задачу. Это не метрика «дефект после релиза», а метрика потерь на непродуктивные ветки работы. Когда память проекта и границы в контексте заполнены, DER обычно снижается: меньше повторных заходов в уже известные тупики и меньше времени на «ложные тропы».
Что показывает высокий DER: чаще всего это провал в контексте и знаниях: агент и человек тратят время на подходы, которые уже когда-то не сработали, либо начинают слишком широкую задачу без достаточной декомпозиции. Лечится через фиксацию тупиков, сужение области задачи и точный подбор релевантного контекста.
ERR (служебная метрика автора), доля закрытых задач, по которым в следующий месяц пришлось заводить отдельную задачу на исправление. Логическая ошибка, пропущенный пограничный случай, поломка в соседнем модуле от своей же правки, всё, что потребовало возврата к уже «готовому». На моих проектах ERR держится в районе шести процентов после того, как ворота сделали обязательными негативные сценарии и проверку критериев на выходе. До этого он был около пятнадцати.
То, что цифры устойчиво улучшаются после введения каждого элемента контура, это наблюдение, а не доказательство. На большой выборке проектов с разными постановщиками результат может выглядеть иначе. Цифры FPSR-серверной и ERR собраны на подмножестве моих проектов: сорок процентов FPSR это ранние проекты, до формализации; семьдесят пять-восемьдесят это серверная часть в знакомой предметной области уже под спецификациями. Не среднее по тридцати с лишним.
Метрики читаются связкой, не по отдельности. Низкий FPSR при низком MIR обычно означает плохие спеки и согласный с этим агент: задача возвращается на доработку, но не из-за правок в коде. Возвращается потому, что результат не тот, который был нужен. Высокий MIR при приличном FPSR означает обратное: задачи берутся с первой попытки, но рукой потом доводятся, потому что контекст не дотягивает. Высокий DER указывает на потери в процессе поиска решения, а высокий ERR при хорошем FPSR и MIR говорит, что внутри одной задачи всё выглядело чисто, но критерии приёмки оказались слишком позитивными.
Главное в метриках не сам уровень. Главное в том, что они дают язык, на котором понятно, что именно сломалось. До того как я начал их считать, плохая неделя выглядела как сплошной хаос. После звучала уже как «MIR пошёл вверх на интеграционных задачах, скорее всего, контекст по новому модулю собран криво». Второе чинится. Первое нет.
Где ворота не помогают
Контур закрывает не всё. На двух типах задач, прямо примыкающих к воротам, он работает плохо, и это надо назвать прямо, прежде чем пойдёт следующая статья.
Задачи на ощущение продукта. Кнопка должна ощущаться отзывчивой, анимация должна быть приятной, текст ошибки должен быть человеческим. Эти критерии не формализуются на уровне, который агент может проверить. Спецификация на такую задачу может быть идеальной по форме, и результат всё равно приходится переделывать на третью итерацию. На Сортуле я так попадал, об этом было в первой статье. Здесь ворота помогают только структурой, не содержанием.
Чужая документация, которая сама себе противоречит. Когда внешний сервис возвращает не то, что обещано в его собственной документации, никакая спецификация на моей стороне этого не вытянет. Сначала приходится руками разобраться, как чужой сервис работает на самом деле, и только потом ставить задачу. QG-0 в этом случае прошёл, агент честно реализовал по тому, что было в задаче, а в проде всё равно сюрприз. Лечится только сбором собственного контракта на интеграцию до постановки задачи.
Есть и более широкие границы методологии: незнакомая постановщику предметная область, постепенная деградация архитектуры на горизонте года, перенос подхода с одного человека на команду. О них целиком пойдёт речь в шестой статье; они уже не про устройство ворот одной задачи. Здесь важно зафиксировать одно: ворота закрывают задачу с двух концов и снимают с человека обязательную часть рутины, но не отменяют ни понимания продукта, ни знания домена, ни умения читать чужие интерфейсы. Этого никакой контур не закроет.
Если унести из статьи одну мысль
Ворота задачи в SENAR это не бюрократический шаблон поверх обычного процесса, а единственный способ, которым личная дисциплина перестаёт быть единственной защитой от собственной пятничной усталости. На входе они отказываются принимать задачу без цели, критериев и негативных сценариев. На выходе они отказываются закрывать задачу, пока критерии не сверены, ручные правки не зарегистрированы и память проекта не пополнена тем, что вскрылось в работе.
Между двумя воротами агент работает не в пустоте. Среда, в которой он живёт, состоит из трёх слоёв: контекст под задачу, явные архитектурные границы, типизированная память проекта. Без них даже идеальная спецификация ломается. Агент в третий раз заходит в задачу с нуля, не знает, какие модули трогать нельзя, и заново наступает на тупик, зафиксированный в чьей-то голове, но не в репозитории. Об устройстве этой среды и о том, почему один документ CLAUDE.md в корне репозитория — это ещё не среда, в следующей статье.
Словарь терминов
Термины этой статьи. Базовые определения (SENAR, TAUSIK, FPSR, MIR, DER, спецификация задачи) даны в словарях первой и второй статей.
Ворота качества. Автоматические проверки, которые блокируют задачу, если условия не выполнены. В контуре одной задачи обязательны два шлюза: QG-0 на входе задачи (нельзя начать без оформленной спецификации) и QG-2 на выходе (нельзя закрыть без сверки с критериями, фиксации ручных правок и обновления памяти проекта). В полном стандарте SENAR шлюзов пять (QG-0..QG-4), но остальные относятся к уровню требований, мержа и релиза.
Спецификация задачи. Формальное описание задачи до того, как агент начнёт писать код. Для контура одной задачи в этой статье обязательны пять секций: цель, критерии приёмки, негативные сценарии, границы изменений, ссылка на архитектурный контекст. В полном SENAR Standard у QG-0 есть дополнительные обязательные пункты (связь с требованием/историей, тип работы, критерий по безопасности для задач риска).
Критерии приёмки. Перечисление того, по чему я подтвержу, что задача решена. Каждый критерий проверяемый, с однозначным исходом. На выходе задачи каждый критерий должен иметь явное подтверждение: тест, ручную пометку или ссылку на артефакт.
Негативные сценарии. Критерии приёмки про то, чего не должно произойти. Выделены в спецификации отдельной обязательной секцией, потому что чаще всего пропускаются.
QG-0, QG-2. Сокращения для входных и выходных ворот качества задачи. В SENAR есть и другие шлюзы более высокого уровня, качество требований истории (QG-1), пред-мерж проверка (QG-3), релизная приёмка (QG-4), но они работают на уровне историй, веток и релизов, а не одной задачи, и в этой статье не разбираются.
Серия про инженерный процесс для разработки с ИИ-агентами:
-
Часть 1: Полтора года без ручного кода: почему инструкции ИИ-агенту не заменяют инженерную дисциплину
-
Часть 2: ИИ-агент — не программист: пять наблюдений и три следствия
-
Часть 4: Спецификация, ворота, метрики: как SENAR закрывает вход и выход задачи
-
Часть 5: Среда агента: контекст, архитектура, память (скоро)
ссылка на оригинал статьи https://habr.com/ru/articles/1029764/