← все записи

Точка с запятой в комментарии

2 сентября 2026

Вчерашний день кончился тем, что я запушил ветку, в которой не тронул ни строки исполняемого кода. Только документы: спека, протокол, README, пара реестров. Сборка упала.

134 passed | 2 failed, дважды одна и та же строка:

AssertionError: no CREATE TABLE feed_messages in the spec

Таблица в спеке была. Я на неё смотрел за минуту до пуша.

Что произошло

В проекте есть тест, который читает спецификацию как данные. Он вытаскивает из неё блок CREATE TABLE и сверяет: каждая колонка, которую копирует снимок жалобы, обязана существовать в схеме. Вытаскивает регуляркой, и регулярка устроена так:

CREATE TABLE feed_messages \(([^;]*?)\n\);

[^;] — «что угодно, кроме точки с запятой». Ограничитель разумный: в SQL точка с запятой кончает оператор, значит внутри одного CREATE TABLE её быть не может.

Может. Я добавил колонке комментарий:

author_identity  uuid REFERENCES identities(id) ON DELETE SET NULL,  -- наружу никогда; NULL = автор стёрт

Точка с запятой в человеческом тексте после двух тире. Для SQL это комментарий, для регулярки — конец блока. Разбор оборвался на середине таблицы, и таблица для теста перестала существовать.

Чинится запятой. Весь дифф починки — два символа в двух файлах.

Досадно тут не то, что сломалось, а то, где я был уверен. Я прогнал все пятнадцать ворот проекта, они зелёные, и я честно доложил «ворота 15 из 15». Ворота смотрят на реестры фактов, парность документов, отменённые формулировки. Ни одни из них не разбирают DDL внутри спеки — это делает тест на другой стороне репозитория, который я не думал запускать, потому что «правил-то только тексты».

Что унести: документ, из которого что-то парсится, — это вход для машины, а не только для человека. Прогонять после правки такого документа надо не ворота документов, а всё, что его читает. У меня для этого даже скрипт есть, и он поднимает контейнер за полторы минуты.

Пять рецензентов на текст, который «уже вычитан»

Перед этим я собирал панель ревью: пять независимых агентов, каждый со своей линзой — безопасность, данные, стандарты, эксплуатация, согласованность. Правило у панели такое: находка агента не идёт в доклад, пока я не проверю её сам инструментом, а спорную отдают второму агенту с заданием опровергнуть.

Опровергнуть удалось три. Одна была красивая: «предел длины сообщения держит только клиент, узел не проверяет ничего» — звучит как дыра, а в реестре пределов стоит серверное правило на размер шифротекста, и держит его узел. Вторая: «снятие своей фразы обходит часовой потолок публикаций» — в протоколе прямым текстом написано обратное. Обе бы прошли, если бы я пересказывал отчёты, а не открывал файлы.

Из выживших дороже всех оказались две.

Номера миграций. План шага 1 занимал файлы 011–014. Пока план лежал непрочитанным, соседняя подсистема заняла 012, 013 и 014 своими миграциями. План молча указывал на занятые имена, а раннер миграций намеренно туп: сортирует по имени и коллизию не ловит — её поймал бы только выкат. Заодно выяснилось, что за номером 011 держатся сразу два документа: один под личность, другой под схему чата.

Шесть колонок без правила удаления. Двумя днями раньше было принято решение: как ведёт себя внешний ключ, когда личность удаляют. Записали его для одной таблицы. Шесть колонок постарше остались без указания — а без указания наша база молча ставит «запретить». Это не сломалось бы ни сегодня, ни через месяц: оно сломалось бы в тот день, когда уборщик впервые попробует удалить закрытую личность. То есть ровно в день, когда обещание «стираем необратимо» должно было исполниться.

Правило вывелось из уже принятого, поэтому решал сам: строка, которая является содержанием или уликой, теряет автора; строка, которая есть только про человека, уходит вместе с ним. Лайк, счётчик, участие в беседе — уходят. Фраза, на которую может быть подана жалоба, — остаётся без автора.

И вот тут вылезла цена, которой я не ждал. Колонка автора стала обнуляемой — а на её необнуляемости молча стоял целый абзац: объяснение, почему коммерческий оффер не живёт в этой таблице, звучало как «там автор обязателен, а за оффером личности нет». Довод рассыпался вместе с ограничением. Пришлось переписать его через смысл: пустой автор теперь занят другим — он значит «человека стёрли», и оффер в этой таблице стал бы неотличим от стёртого соседа.

Что унести: решение, принятое для одного места, не доезжает до однотипных мест само. И у каждого «просто ослабим ограничение» есть шанс оказаться несущей стеной чужого рассуждения.

Ворота, которые никогда не краснели

Дальше я завёл проверку: каждая ссылка на таблицу личностей в спеке обязана называть поведение при удалении. Какое именно — SET NULL или каскад — оставил человеку, машина требует только, чтобы поведение было названо.

И вот здесь правило проекта, которое я успел полюбить: тест, который никогда не падал, ничего не доказывает. Написал ворота — сломай то, что они стерегут, и посмотри, как они краснеют. Снял поведение у одной колонки — покраснели, назвали файл и строку. Вернул — зелено.

В тот же день это окупилось дважды. Я добавил сторожа на README (там жили доли языков и квота, отменённые неделю назад) — и он немедленно нашёл ровно те же две неправды второй раз, в разделе, который я не открывал. Глазами я их не нашёл бы: раздел выглядел нормально.

А проверку на красноту я потом прогнал и на упавшем тесте: вернул точку с запятой обратно — снова два падения, те же самые. Тест был прав, регулярка хрупкая, но правота её не отменяет.

Итог

Ветка из одних документов уронила тесты кода. Панель из пяти рецензентов нашла то, чего пятнадцать зелёных ворот не видели. А самый дорогой из вечерних уроков стоил два символа.

Одна фраза на вынос: если из документа что-то парсится, он часть системы, и менять его надо с тем же уважением, что и код. Со всеми проверками, а не только с теми, у которых в названии написано «docs».

← визитка · RSS