Every button reaches its handler
A new `ui_actions` guard follows each button through its dispatch tables to its handler and a declared API route; `crossweft actions` prints the chain.
One new guard: buttons and other UI controls are now a seam Crossweft can check.
Why. A button names its action with a string written by hand: a data-action, a callback name, a menu command. The code that runs it is found through more hand-written strings: a lookup table, a registry, a prefix rule. Rename the action on one side only and nothing fails to compile. The button stays on the page and does nothing, or a handler stays in the code that no button calls any more.
ui_actions. A ui_actions entry in the map says where the controls are and which tables dispatch them. Crossweft follows each control through those tables to its handler, and with "routes": true on to an API route a link declares, so the route checks keep that route served. It reports four problems:
ui:<id>:dead: no handler takes the control;ui:<id>:unrouted: the chain reaches no declared route;ui:<id>:orphan: a handler no control sends to any more;ui:<id>:conflict: one key bound twice to different values.
A control that is meant to end without a route can be excused with allow and a reason.
crossweft actions. It prints every control's chain: which table took it, in which file, and the route at the end. --json gives the same for a script.
Static check and a click test. The check proves the wiring in the source. It cannot prove that a handler does not throw or that a button is not hidden under another element. The demo repository now has an admin page and smoke/click_smoke.py, which reads crossweft actions --json, presses every mapped button in Chromium and waits for the request the map expects. Its self-test must turn red on a handler that throws.
After upgrading, run crossweft agents again: the agent skill now explains the ui: problems and crossweft actions.
Source: v0.2.10 · CHANGELOG.md