Skip to main content
Glama

Basanos

ci license: MIT node 20+

English · Português

The dead-button detector. Does this control call a handler that actually exists — and does something?

Βάσανος (basanos) was the touchstone the ancient Greeks rubbed gold against to tell real from fake. This one rubs your UI controls against your handlers: for every onClick, onSubmit, onClicked, it resolves the target and tells you whether that target is WIRED (exists, has a real body), DEAD (called but doesn't exist — a broken button), or STUB (exists but does nothing).

It's static, deterministic, and $0 — it reads the code, it doesn't run your app. No LLM. No network. No hallucination.

Built for the loop, not the post-mortem

A dead button is rare in finished code — QA catches it. But it's common in the half-built UI an agent is writing right now: it drops the <button> before the handler exists, or forgets to wire it. Basanos isn't a linter for shipped apps — it's the check an agent runs while it builds the screen, on the diff it just wrote, so it fixes the dead control before it says "the UI is done."

Primary use — the agent's self-check (MCP). Run Basanos as an MCP server inside your coding agent's loop: after writing a component the agent asks "is every control I just added wired to something real?" and can't declare the UI finished while a control is DEAD. (Also a CLI, pre-commit hook, and CI gate.)

Who it helps most: weaker, cheaper, autonomous models. A frontier model usually remembers to wire its handlers. Basanos catches the moments a model thinks the button works but left it dead — widest on cheap models running long, on their own, with nobody clicking through the UI. It's deterministic and free, so it's viable on every step. On a top model it's a seatbelt; on a cheap autonomous one it's what stops shipped-but-dead controls.

One of three, standalone or an eye for Platão. Basanos is its own product — the touchstone for UI wiring. Its siblings: Platão ("did you actually finish?" — completeness & placebo detection) and Socrates ("do your tests catch bugs, and is your API proven?"). Install Basanos alongside Platão and Platão's ui_wired question delegates to Basanos automatically.


Related MCP server: UIZZE

30 seconds

Requires Node.js 20+ and git. Basanos is not on the npm registry yet, so install it from GitHub (it builds itself on install):

npm install -g github:Owxessus/basanos
basanos audit src/

This is the real output on the seeded example in this repo (examples/agent/demo/Cart.tsx):

$ basanos audit examples/agent/demo
Basanos - examples/agent/demo/Cart.tsx
  [x] DEAD examples/agent/demo/Cart.tsx:13  onClick={checkout} — handler 'checkout' does not resolve to a real function (dead control)
  [!] STUB examples/agent/demo/Cart.tsx:15  onClick={() => {}} — handler has an empty body (the control does nothing)

1 dead . 1 stub

A clean tree prints OK Basanos: every control resolves to a real handler. The exit code is what CI and hooks read:

Exit

Meaning

0

nothing at or above --fail-on (default high, i.e. no DEAD control)

1

at least one finding at or above --fail-on

2

usage error: unknown flag value, or a path that doesn't exist

--json prints the findings as a JSON array (handler, target, verdict, severity, file, line, message); --fail-on high|medium|low|never sets the threshold (DEAD is high, STUB and PLACEBO are medium, an unparseable file is low).

git clone https://github.com/Owxessus/basanos && cd basanos
npm ci            # also builds dist/
npm test          # the proof suite
node dist/cli.js audit examples/agent/demo

A dead button is the most common "looks done, does nothing" defect in a UI. Basanos checks every control in one deterministic pass, and where it cannot resolve a handler it says UNKNOWN instead of guessing (see Known limits below).


Why this exists

Every developer has shipped a button that does nothing. An agent generating UI does it constantly — it writes onClick={handleSave} and forgets to write handleSave, or writes it as an empty arrow, or wires two different buttons to the same action. The app compiles. The button renders. Nothing happens when you click it, and you find out from a user.

The concept is framework-agnostic: control → handler → does the target exist and do real work? Basanos implements it for the frameworks where dead handlers hurt most.

Supported today: React / JSX / TSX (.js, .jsx, .tsx), Vue SFCs (.vue, <script setup> and Options-API methods), Svelte (.svelte, on:click and Svelte-5 onclick), and Qt/QML (.qml — empty and do-nothing interaction handlers). Cross-file handler resolution (follow the import, check the real function in the other file) applies to React, Vue and Svelte.


What it reports

For every event handler it finds:

Verdict

Meaning

WIRED

The handler resolves to a function that exists and has a real body.

DEAD

The handler is called but the target does not exist — a broken control.

STUB

The target exists but its body is empty (() => {}) — a button that does nothing.

PLACEBO

Two distinct controls fire the same concrete action (fn('id')) — copy-paste, one effect.

UNKNOWN

The receiver isn't a resolvable local/imported symbol — reported honestly rather than claimed "wired." We never assert what we can't prove.

It also flags placebo wiring: two distinct controls firing the same action with the same argument — often a copy-paste where "Approve" and "Discard" both call dispatch('open').

Everything is deterministic and static — it parses source, resolves handlers by scope and imports, and classifies bodies by AST. It never launches your app, so it's safe to run on every commit and in CI.


Usage modes

Mode

Command / setup

CLI

basanos audit . · basanos audit src/Cart.tsx

Pre-commit hook

basanos install-hook — block a commit that introduces a dead button (audits the staged files; honours core.hooksPath, and refuses to overwrite an existing hook)

CI gate

GitHub Action: uses: Owxessus/basanos@main — fail the build on findings ≥ --fail-on (a new-only ratchet is planned)

MCP server

basanos mcp — exposes basanos_audit so any agent checks its own UI before "done"

As a Platão eye

Install both; Platão calls Basanos for ui_wired automatically

CI — the action builds Basanos from the ref you pin, so it needs nothing from npm:

# .github/workflows/ui-wiring.yml
on: [push, pull_request]
jobs:
  basanos:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: Owxessus/basanos@main   # pin a tag or commit for reproducible runs
        with:
          paths: src          # space-separated files/directories (default ".")
          fail-on: high       # high | medium | low | never

MCP — register the stdio server with your client. In Claude Code: claude mcp add basanos -- basanos mcp; in any client that reads an mcpServers config:

{ "mcpServers": { "basanos": { "command": "basanos", "args": ["mcp"] } } }

The tool basanos_audit takes path (file or directory) and optional failOn, and returns { ok, failOn, summary, findings }. It reads the same .basanos.json as the CLI, from the server's working directory.

There's a complete, runnable UI-auditing agent in examples/agent/: a Claude Code project that wires Basanos as an MCP server and gives an agent one rule — build the UI, then audit it, then fix, and only then say "done". It ships with a seeded-broken component so you watch the tool fire on the first run.

Cost

$0. Basanos is purely deterministic — AST + static resolution, no LLM anywhere. There is no judgment layer and no API bill. That's the whole design: wiring is a structural fact, not a matter of opinion.

Configuration

Basanos auto-detects the framework per file and needs no config to run. Optionally, a .basanos.json in the directory you run it from (your repo root, typically) extends the audited handler set and ignores files by glob:

{
  "handlers": ["onSwipe"],
  "ignore": ["**/*.stories.tsx", "**/*.test.tsx"]
}

handlers adds JSX attributes to the React set (onClick, onSubmit, onChange, onInput, onKeyDown, onKeyUp, onMouseDown, onMouseUp, onDoubleClick, onFocus, onBlur, onToggle, onDrop, onDragStart); Vue, Svelte and QML use their own fixed event lists. node_modules, .git, dist, build, .next, coverage and .turbo are always skipped.

Known limits — it is static, so it says UNKNOWN (silent) rather than guess: member handlers (onClick={this.save}, onClick={store.save}), handlers built by calls, path aliases (@/…) and packages are not followed; in Nuxt / unplugin-auto-import projects an unresolved Vue name is UNKNOWN, not DEAD. A bare browser global used as a handler (onClick={alert}) is reported DEAD.


Contributing — the same rigid gate

See CONTRIBUTING.md. Every framework adapter or classifier must ship with:

  1. Deterministic — no LLM, no network.

  2. prove_effect — a fixture with a DEAD/STUB control, and an assertion Basanos catches it.

  3. negative_control — a fixture where every control is WIRED, and an assertion Basanos stays silent.

  4. Declared severity.

CI runs both proofs on every PR. No proof, no merge. A wiring auditor that mis-reports wiring is worse than none.


Proven, not asserted

Anti-placebo is a rule this project holds itself to. Every check ships with a prove_effect + negative_control pair — it must catch the real defect and stay silent on the honest twin, or it doesn't ship (CI enforces it). And CI runs the built CLI on the seeded example and requires it to catch the dead button there.

It was then tuned by running it over a range of open-source front-ends. Where it flagged a real dead control, good; where it flagged a wired one, that pattern became a fix with a regression test. False positives still happen — when you hit one, an issue with the snippet is the most useful contribution there is.

Where this came from

Basanos was extracted from Athena, a personal AI agent project, where it checks the interface the agent builds for itself: "is this button wired to something real?" is asked before a screen is called done. It is released on its own because dead controls show up wherever agents write UI. It has no dependency on the rest of Athena.

License

MIT, plus the CONTRIBUTING.md proof gate.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Enables agents to validate UI designs for missing states, accessibility issues, and journey completeness using state machine modeling.
    17
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    STOP UI SLOP. Gives coding agents searchable evidence from 800,000+ real web and iOS screens, design contracts, and a hard UI finish gate.
    1
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Provides read-only MCP tools for discovering UI libraries, recommending minimal compatible UI stacks, and auditing plans to enforce deterministic UI policy and quality gates for coding agents.
    7
    112 npm
    1
    MIT