happyin.work / блог

English · Русский

Установите и попробуйте демо швов на странице проекта

Кнопка, которая ничего не вызывает, всё равно собирается. 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. Об этой идее мы писали в статье Каждому рукописному шву нужен страж.

Кнопка — такое же место, только длиннее. Между нажатием и сервером обычно стоит цепочка рукописных поисков:

  1. элемент называет своё действие (data-action, имя колбэка в чат-боте, команда меню);
  2. таблица сопоставляет это имя обработчику, или правило по префиксу (startsWith("export:")) берёт целое семейство имён;
  3. обработчик обращается к маршруту.

Эту цепочку не видит ни один компилятор. Проверка типов видит, что 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.

Другие статьи

Ещё на сайте

Anastasiia Butova - ML engineer, Belgrade, Serbia. Email [email protected] · LinkedIn · GitHub · Telegram · Hugging Face