Правила, которые нельзя нарушить: как сделать разработку сходящейся
Почему одни и те же дефекты возвращаются тикетами, даже когда задачи поставлены внятно, и как перестроить процесс — реестр инвариантов, единая граница записи, тест «входы × правила» и обязательный гейт. Практика из наших проектов и типовых случаев.
Знакомая картина: команда чинит одно — ломается другое. Заказчик заводит тикет за тикетом по одному и тому же экрану, в заголовках появляются слова «опять», «снова», «уже просили». Работа идёт быстро, релизы каждый день, а ощущение — что стоим на месте. Ниже — разбор, почему так происходит, и что конкретно надо изменить в архитектуре разработки, чтобы процесс стал сходящимся: чтобы каждая закрытая задача уменьшала будущий поток, а не порождала следующую.
Материал — из нашего опыта и типовых случаев, которые мы видели на проектах. Ни один из выводов не про «плохих разработчиков»: почти всегда причина устроена так, что в ней некого винить, — и именно поэтому её не замечают годами.
Сначала снимите ложный диагноз
Первое объяснение, которое приходит в голову владельцу продукта: «наверное, я плохо ставлю задачи». Обычно это неправда, и это стоит проверить, а не обсуждать.
Проверяется дёшево. Возьмите свой корпус задач за полгода, разделите его на короткие и подробные (по длине описания, по наличию шагов воспроизведения, скриншотов, ожидаемого результата) и посмотрите, чаще ли после коротких задач прилетает новая задача по тому же месту в течение двух недель. В типовом случае разницы нет: подробные описания дают ровно такой же процент возвратов, как и короткие. Значит, дело не в постановке.
Второй кандидат — «слабая аналитика, требования всплывают поздно». Отчасти да: в живом продукте правила действительно открываются по ходу, особенно там, где логика эвристическая — планирование, ценообразование, распределение нагрузки. Но это нормальный способ жизни продукта. Грех не в том, что правило открылось поздно, а в том, что открытое правило не перевели в исполняемую форму — оно осталось абзацем.
Три настоящие причины
1. Правила живут в прозе
Регламент, гайд, комментарий в коде, договорённость в переписке. Прозу нельзя нарушить — её можно только не прочитать. Проверять её исполнение некому: ни компилятор, ни тест, ни CI про неё не знают.
Дальше правило начинают дублировать условиями по месту: здесь проверили, там проверили, в новом обработчике забыли. Мы регулярно встречаем ситуацию, когда одно бизнес-правило размазано сотней с лишним условий по одному большому модулю — и всё равно нарушается, потому что сто первый обработчик написали в пятницу.
2. Нет единой границы записи
Классическая архитектурная ошибка: состояние можно изменить из десятков мест. Каждый новый путь записи по умолчанию бесправен — он может записать что угодно, и корректность держится на памяти автора.
Симптом, по которому это видно снаружи: правило нарушается каждый раз в новом месте. Починили в кнопке «Сохранить» — вылезло в drag-and-drop; починили там — вылезло в массовой операции. Тикеты выглядят как разные дефекты, а причина одна.
3. Регрессионная сеть есть, но ни к чему не подключена
Отдельный, самый обидный случай. Тесты писались годами, лежат в репозитории, стоят потраченного времени — и никто их не гоняет. Ни в CI, ни перед мержем. Часть из них давно красная, и никто не знает, какие именно красные означают «сломалась функция», а какие — «тест устарел», потому что суд не заседает.
Это ровно то состояние, в котором «чиним одно — ломаем другое» становится неизбежным: обратная связь о поломке приходит не через 30 секунд от теста, а через сутки от заказчика.
И отдельно — для тех, у кого код пишет ИИ-агент
Здесь появляется четвёртая причина, которой не было десять лет назад. Агент читает точку входа — файл с инструкциями репозитория — и делает буквально то, что там написано. Если в нём источником истины назван документ, описывающий, как сделано сейчас, агент пойдёт именно туда, а ваше техзадание, описывающее, как должно быть, останется непрочитанным. Не потому что агент ленив, а потому что вы так написали.
Отсюда простое, но неочевидное следствие: точка входа — часть архитектуры, а не файл с заметками. Она определяет, какой документ побеждает в споре.
Как надо: шесть решений
1. Разделите нормативный документ и карту кода
Это два разных жанра, и путать их нельзя:
- Техзадание — что должно быть верно. Согласовано с заказчиком, меняется только вместе с его решением.
- Карта кода (описание алгоритма, архитектурный обзор) — как сделано сейчас. Растёт вместе с кодом, нормативной силы не имеет.
Карта кода не может быть нарушена по определению — она всегда «права», потому что описывает факт. Поэтому источником истины она быть не может, а именно её обычно и назначают источником истины: она подробнее, свежее и написана инженерами.
Правило спора: код разошёлся с ТЗ — прав ТЗ, расхождение оформляется задачей. Не наоборот.
2. Различайте запрет и предпочтение
Половина бесконечных тикетов растёт из терминологической путаницы. «Дорогие заказы обслуживаются первыми» — это запрет или пожелание? Если в системе это реализовано весом в формуле приоритета, то любой случай, когда другой вес пересилил, будет выглядеть как дефект — и приносить новый тикет.
Заведите жёсткое соглашение: слово «инвариант» значит «проверяется машиной и запрещено всегда». Всё остальное — предпочтение со своим весом, и его «нарушение» закрывается не правкой кода, а показом чисел: вот сколько стоил этот вариант, вот сколько альтернативный.
3. Сделайте реестр инвариантов кодом
Не абзац, не комментарий, а список в отдельном модуле. У каждого правила:
- идентификатор — им называют нарушение в логе и в разговоре;
- ссылка на пункт ТЗ — чтобы правило можно было оспорить в правильном месте;
- область действия — кого ограничивает. Часто оказывается, что запрет адресован автоматике, а человек вправе его обойти осознанно (и наоборот);
- чистая функция проверки — без обращения к экрану и глобальному состоянию, чтобы её можно было звать и в бою, и в тесте.
Полезный приём — правило-наблюдатель: новое правило добавляется в реестр в режиме «только считать», пишет нарушения в журнал, но ничего не блокирует. Через неделю по журналу видно, срабатывает ли оно на настоящих нарушениях или на законных исключениях, о которых вы не подумали. Потом включается запрет.
4. Сведите запись к одному шлюзу
Все пути изменения состояния — кнопки, автоматика, массовые операции, импорт — должны сходиться в одну функцию, которая:
- собирает предлагаемое изменение;
- прогоняет реестр инвариантов;
- при нарушении отказывает и называет правило;
- и только потом пишет.
Это тот самый архитектурный шаг, после которого класс дефектов «в новом месте забыли проверку» исчезает физически: забыть проверку негде, она не в обработчике, а на границе.
Побочный эффект, который недооценивают: нарушение перестаёт быть тикетом и становится сообщением об ошибке с именем правила. Разница в цене — сутки против секунды.
5. Тестируйте правила, а не кнопки
Типовая ошибка — тест на тикет: «после нажатия такой-то кнопки в таком-то случае происходит вот это». Такой тест ловит вчерашний дефект в том месте, где его заметили, и молчит про новую кнопку.
Правильная форма — таблица «входы × правила»: каждое правило из реестра прогоняется по каждому входу системы. Новый вход добавляется в таблицу одной строкой и автоматически проверяется всеми правилами; новое правило — одной строкой и проверяется на всех входах. Обработчик, забывший про инвариант, роняет тест по построению, а не по счастливой случайности.
И обязательно проверьте, что тест умеет краснеть: временно снимите правило из реестра и убедитесь, что тест упал. Зелёный тест, который никогда не был красным, ничего не гарантирует.
6. Сделайте прогон обязательным — и быстрым
Тесты, которые не блокируют мерж, — это документация, а не защита. Три требования:
- обязательный статус-чек на ветке, куда вливается работа;
- скорость: если полный прогон занимает минуты, его будут обходить. Секунды — не будут;
- карантин для долга: сегодняшние красные тесты выносятся в явный список, который только сокращается. Дописывать в него нельзя — новый красный тест чинится или удаляется в том же изменении.
Карантин важнее, чем кажется: без него включение гейта откладывается «пока разберём красноту», то есть навсегда.
Тикет должен производить правило, а не патч
Отдельно про формат задач — и здесь мы возвращаемся к ложному диагнозу из начала статьи. Подробность описания не влияет на возвраты, а вот форма влияет.
Задача, названная по имени файла или экрана («вот здесь поправьте вот это»), производит патч. Задача, названная правилом, производит правило. Минимальный шаблон, который окупается:
- Правило — одна проверяемая фраза в настоящем времени: что должно быть верно всегда.
- Область действия — все входы, где правило обязано соблюдаться, а не только тот, где вы заметили нарушение. Это самый ценный пункт: заказчик обычно знает список лучше разработчика.
- Как воспроизвести — данные и действия, достаточные, чтобы собрать фикстуру.
- Тип — жёсткое правило / предпочтение / разовый дефект / вопрос. От типа зависит, попадёт ли правило в реестр.
- Приёмка — имя падающего теста, который пишется первым.
И правило разбиения: одна задача — одно правило. Задача из пяти пунктов закрывается по первому, остальные четыре возвращаются через неделю новыми задачами — это и есть механизм, который создаёт ощущение бесконечности.
Как понять, что процесс стал сходящимся
Ощущениям здесь верить нельзя — мерьте. Пять метрик, которые считаются по вашему же трекеру и репозиторию:
- Доля повторов по месту. Какой процент задач приходит туда, где за последние 30 дней уже была задача. Если этот процент не снижается месяц за месяцем — сходимости нет, что бы ни говорили ощущения.
- Доля смысловых дублей. Процент задач, у которых в истории есть близкий по смыслу предшественник. Считается эмбеддингами, но с обязательной калибровкой: сначала измерьте, какое сходство дают случайные пары задач из вашего корпуса, и только потом выбирайте порог. Абсолютные значения косинуса не переносятся между корпусами и моделями — переносится процедура.
- Время от правки до следующей задачи по тому же месту. Медиана в часах. Короткая медиана — признак тугой петли без фильтра: правка уходит в бой, дефект возвращается вопросом.
- Красных тестов в карантине. Число, которое обязано убывать, и график, который стыдно показывать растущим.
- Доля изменений, прошедших через обязательный чек. Здесь допустимо только 100%.
Первые две метрики отвечают на вопрос «сходится ли», три остальные — «работает ли механизм, который должен обеспечивать сходимость».
Порядок внедрения
Ставьте дешёвое первым — иначе не доживёте до дорогого:
- Гейт с карантином — часы работы, эффект сразу. Даже без единого нового теста вы перестаёте ломать то, что уже покрыто.
- Иерархия документов и точка входа — часы. Особенно если код пишет ИИ-агент.
- Шаблон задачи и правило «сначала падающий тест» — привычка, а не работа.
- Реестр инвариантов — дни. Начните с двух-трёх правил, которые возвращались чаще всего; полный список не нужен.
- Шлюз записи — дни, и это самый рискованный шаг. Его нельзя делать одним изменением вместе с реестром: если поедет, разбирать будет нечего.
- Тест «входы × правила» — день после реестра.
Что вы получаете
Правило, которое негде нарушить громко, будет нарушаться тихо — и возвращаться задачами, пока не надоест. Правило, вынесенное в реестр и проверяемое на границе записи, нарушается один раз: в момент написания кода, с сообщением об ошибке и именем правила.
Это и есть архитектурно верная разработка в одном предложении: инварианты — в код, запись — в одну дверь, проверка — в обязательный гейт, задача — в правило. Всё остальное — следствия.
Особенно это касается проектов, где код пишет ИИ-агент. Скорость правок в них выше скорости ревью, и единственный способ удержать систему — сделать так, чтобы правила проверяла машина, а не память человека, который сегодня в отпуске.