happyin.work / blog

English · Русский

Install it and try the seam demo on the project page

A button that calls nothing still compiles. Crossweft now follows every button to its handler and its API route

By Anastasiia Butova, ComfyUI and diffusion-model engineer, Belgrade · published 28 October 2026

A button and the code it runs are joined by a string that someone typed twice. The page says data-action="place-order", a table in another file says "place-order": placeOrder, and the handler calls /v1/orders. Rename the string on one side and the build stays green, the tests stay green, and the button does nothing. Crossweft 0.2.10 adds a check for exactly this seam: ui_actions follows every control through the tables that dispatch it to its handler and on to an API route the map declares.

Why buttons are a seam

Crossweft checks the places where two parts of a system must agree on something written by hand: a route, a header, a version string, the fields of a DTO. We wrote about the idea in Every hand-written seam needs a guard.

A button is the same kind of place, only longer. Between the click and the server there is usually a chain of hand-written lookups:

  1. the control names its action (data-action, a chat bot's callback name, a menu command);
  2. a dispatch table maps that name to a handler, or a prefix rule (startsWith("export:")) takes a whole family of names;
  3. the handler calls a route.

No compiler sees the chain. A type checker sees that placeOrder exists, not that the string on the button still leads to it. A coding agent that renames an action in one file and runs the tests has no reason to open the other two.

What the check does

A ui_actions entry in the map says where the controls are and which tables dispatch them, each table described by a regular expression that is tried against the real files:

{"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 follows each control hop by hop and reports four kinds of problem:

Problem Meaning
ui:<id>:dead no handler takes the control
ui:<id>:unrouted the chain ends without reaching a route a link declares
ui:<id>:orphan a handler that no control sends to any more
ui:<id>:conflict one key bound twice to different values; the later one silently wins

With "routes": true the route at the end of the chain must be one that a link declares, so the existing route checks also make sure the server still serves it. crossweft actions prints the chain of every control, and --json gives it to a script.

The check is written to fail loudly rather than pass quietly. A pattern that extracts nothing is an error, not an empty list. A control built entirely at run time (${kind}) is dead, because nothing can prove that every value it builds is handled; give it a static prefix or excuse it with allow and a reason. A template route such as /v1/${kind} is read as a route with a parameter, not as "anything under /v1/". Two independent reviews ran adversarial maps against the check before release; each silent pass they found became a test.

Static proof and a click test

Reading the source proves the wiring. It cannot prove that the handler does not throw, that the button is not covered by another element, or that the page does not build a button the map does not know. So the demo repository also has a small click test, smoke/click_smoke.py. It reads crossweft actions --json, opens the page in Chromium, presses every mapped button and waits for the request the map expects. Its self-test plants a handler that throws and must turn red. The two checks answer different questions, and we run both.

What it found in two of our codebases

Before release we ran the prototype in a sandbox on copies of two of our own projects, with agents writing the maps and a separate reviewer checking the results.

A production chat bot. The map covered 166 controls: inline buttons (53 of them built from a prefix), reply buttons and prepared menus. The worry we started with, a button that lost its handler, did not show up: every live button reached a handler. The reverse did. Seventeen handlers had no live button. Six of those buttons were commented out, eleven were never sent at all. That is dead code nobody had noticed, and a feature that users cannot reach.

An internal web app (a FastAPI back end and a plain JavaScript front end). Here the route side found real drift: four calls from the front end that the back end does not serve. The upload form posts to a route that does not exist, the batch page polls one that does not exist, the accept and reject buttons post to a third, and the redirect after upload goes to a page nothing renders. Each was reported twice, by the route check and by a set guard. We also planted four changes on purpose. Three were caught with the expected problem. The fourth, renaming a path parameter, was not, and that is by design: {id} and {batch_id} are the same route to the check.

What it cannot see

The same runs showed the limits, and we would rather state them here than have you find them:

  • Compiled UI. The operator dashboard of the web app was compiled JSX that was not in the sandbox copy. About two hundred of its routes were therefore not checked at all, and the report said so rather than passing them.
  • Wiring the patterns cannot express yet. The runs asked for exact-match rules, in [...] lists, buttons whose text comes from a lexicon, getElementById(...).addEventListener(...) wiring and served-versus-declared routes. These are the next iteration.
  • The HTTP method. The click test matches the request path, not its method, because the map does not record a method yet.
  • Behaviour. A green check means every chain is wired. It does not mean the handler does the right thing.

Try it

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

The polyglot demo has the admin page, the map entry and the click test. Rename a data-action in examples/polyglot-shop/web/public/index.html and check names the dead button and the orphaned handler. If you already use Crossweft, run crossweft agents after upgrading so the agent skill knows the ui: problems. Release notes: 0.2.10. Source: github.com/AnastasiyaW/crossweft.

More posts

Elsewhere on this site

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