Кнопка, которая ничего не вызывает, всё равно собирается. Crossweft теперь ведёт каждую кнопку до обработчика и маршрута API
Автор: Anastasiia Butova, инженер по ComfyUI и диффузионным моделям, Белград · опубликовано 2026-10-28
Перевод английской версии: English
Кнопку и код, который она запускает, связывает строка, которую кто-то напечатал дважды. На странице написано data-action="place-order", в таблице в другом файле — "place-order": placeOrder, а обработчик обращается к /v1/orders. Переименуйте строку с одной стороны — сборка зелёная, тесты зелёные, а кнопка ничего не делает. В Crossweft 0.2.10 появилась проверка ровно этого шва: ui_actions ведёт каждый элемент управления через таблицы, которые его разбирают, до обработчика и дальше, до маршрута API, объявленного в карте.
Почему кнопка — это шов
Crossweft проверяет места, где две части системы должны совпадать в том, что написано руками: маршрут, заголовок, строка версии, поля DTO. Об этой идее мы писали в статье Каждому рукописному шву нужен страж.
Кнопка — такое же место, только длиннее. Между нажатием и сервером обычно стоит цепочка рукописных поисков:
- элемент называет своё действие (
data-action, имя колбэка в чат-боте, команда меню); - таблица сопоставляет это имя обработчику, или правило по префиксу (
startsWith("export:")) берёт целое семейство имён; - обработчик обращается к маршруту.
Эту цепочку не видит ни один компилятор. Проверка типов видит, что placeOrder существует, но не видит, что строка на кнопке всё ещё к нему ведёт. Агенту, который переименовал действие в одном файле и прогнал тесты, незачем открывать два других.
Что делает проверка
Запись ui_actions в карте говорит, где лежат элементы управления и какие таблицы их разбирают. Каждая таблица описана регулярным выражением, которое проверяется на настоящих файлах:
{"id": "admin-buttons", "link": "web-orders", "routes": true, "orphans": true,
"controls": {"label": "buttons", "paths": ["web/public/index.html"],
"regex": "data-action=\"([a-z-]+)\""},
"hops": [
{"label": "ACTIONS table", "paths": ["web/public/app.js"],
"within": "const ACTIONS = \\{([^}]*)\\}", "regex": "\"([a-z-]+)\":\\s*(\\w+)"},
{"label": "route the handler fetches", "paths": ["web/public/app.js"],
"regex": "async function (\\w+)\\(\\)\\s*\\{[^}]*?fetch\\(\\s*[\"`]([^\"`]+)"}]}
crossweft check проходит цепочку каждой кнопки шаг за шагом и сообщает о четырёх видах проблем:
| Проблема | Что значит |
|---|---|
ui:<id>:dead |
ни один обработчик не принимает кнопку |
ui:<id>:unrouted |
цепочка кончается, не дойдя до маршрута, объявленного на связи |
ui:<id>:orphan |
обработчик, к которому больше не ведёт ни одна кнопка |
ui:<id>:conflict |
один ключ привязан дважды к разным значениям; молча побеждает последний |
С "routes": true маршрут в конце цепочки обязан быть объявлен на связи, поэтому уже существующие проверки маршрутов заодно следят, чтобы сервер его обслуживал. crossweft actions печатает цепочку каждой кнопки, а --json отдаёт её скрипту.
Проверка устроена так, чтобы падать громко, а не проходить тихо. Шаблон, который ничего не извлёк, — ошибка, а не пустой список. Кнопка, имя которой целиком строится во время работы (${kind}), считается dead: ничто не может доказать, что каждое построенное значение обработано. Дайте ей статический префикс или разрешите через allow с причиной. Маршрут-шаблон вроде /v1/${kind} читается как маршрут с параметром, а не как «что угодно под /v1/». До релиза независимые ревьюеры гоняли против проверки враждебные карты; каждый найденный ими тихий проход стал тестом.
Статическое доказательство и проверка нажатием
Чтение исходников доказывает связи. Оно не может доказать, что обработчик не падает, что кнопку не закрыл другой элемент и что страница не строит кнопку, которой нет в карте. Поэтому в демо-репозитории есть и маленькая проверка нажатием, smoke/click_smoke.py. Она читает crossweft actions --json, открывает страницу в Chromium, нажимает каждую кнопку из карты и ждёт запрос, который ожидает карта. Её самопроверка подсовывает падающий обработчик и обязана покраснеть. Две проверки отвечают на разные вопросы, и мы запускаем обе.
Что нашлось в двух наших проектах
До релиза мы прогнали прототип в песочнице на копиях двух своих проектов: карты писали агенты, результаты проверял отдельный ревьюер.
Рабочий чат-бот. Карта охватила 166 элементов: inline-кнопки (53 из них строятся из префикса), кнопки клавиатуры и готовые меню. Опасение, с которого мы начинали, — кнопка потеряла обработчик — не подтвердилось: каждая живая кнопка доходит до обработчика. Подтвердилось обратное. У семнадцати обработчиков нет живой кнопки: шесть кнопок закомментированы, одиннадцать не отправляются вовсе. Это мёртвый код, которого никто не замечал, и функции, до которых пользователь не может добраться.
Внутреннее веб-приложение (бэкенд на FastAPI и фронтенд на обычном JavaScript). Здесь сторона маршрутов нашла настоящий дрейф: четыре обращения фронтенда, которые бэкенд не обслуживает. Форма загрузки отправляет запрос на несуществующий маршрут, страница пакета опрашивает несуществующий маршрут, кнопки «принять» и «отклонить» пишут в третий, а переход после загрузки ведёт на страницу, которую ничто не отдаёт. Каждое расхождение нашлось дважды: проверкой маршрутов и проверкой набора. Ещё мы нарочно внесли четыре изменения. Три поймались с ожидаемой проблемой. Четвёртое, переименование параметра пути, не поймалось, и так задумано: для проверки {id} и {batch_id} — один и тот же маршрут.
Чего проверка не видит
Те же прогоны показали границы, и лучше назвать их здесь, чем ждать, пока вы на них наткнётесь:
- Скомпилированный интерфейс. Панель оператора в веб-приложении была скомпилированным JSX, которого не было в копии для песочницы. Около двухсот её маршрутов поэтому не проверены вовсе, и отчёт сказал об этом прямо, а не пропустил их как зелёные.
- Связи, которые шаблоны пока не выражают. Прогоны попросили правила точного совпадения, списки
in [...], кнопки с текстом из словаря, связи черезgetElementById(...).addEventListener(...)и сравнение обслуживаемых маршрутов с объявленными. Это следующая итерация. - HTTP-метод. Проверка нажатием сверяет путь запроса, но не метод: карта пока не хранит метод.
- Поведение. Зелёная проверка значит, что каждая цепочка связана. Она не значит, что обработчик делает правильное.
Попробовать
git clone --branch v0.2.10 --depth 1 https://github.com/AnastasiyaW/crossweft
cd crossweft
python -m crossweft --root examples/polyglot-shop actions
python -m crossweft --root examples/polyglot-shop check
В демо polyglot-shop есть страница администратора, запись в карте и проверка нажатием. Переименуйте data-action в examples/polyglot-shop/web/public/index.html, и check назовёт мёртвую кнопку и осиротевший обработчик. Если вы уже пользуетесь Crossweft, после обновления запустите crossweft agents, чтобы навык агента знал проблемы ui:. Заметка о релизе: 0.2.10. Исходники: github.com/AnastasiyaW/crossweft.