basanos
by Owxessus
README.md
# Basanos
<img src="assets/banner.png" alt="Basanos — a static dead-button detector for React, Vue, Svelte and QML">
[](https://github.com/Owxessus/basanos/actions/workflows/ci.yml) [](LICENSE) [](package.json)
**English · [Português](README.pt-BR.md)**
**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](https://github.com/Owxessus/platao).** Basanos is its own product — the touchstone for UI wiring. Its siblings: **Platão** ("did you actually finish?" — completeness & placebo detection) and **[Socrates](https://github.com/Owxessus/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.
---
## 30 seconds
<img src="assets/demo.svg" alt="Basanos — a real run: type the command, see the real output" width="640">
Requires **Node.js 20+** and git. Basanos is not on the npm registry yet, so install it from GitHub (it builds itself on install):
```bash
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`](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`).
<details>
<summary>From a clone instead</summary>
```bash
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
```
</details>
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:
```yaml
# .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:
```json
{ "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/`](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:
```json
{
"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](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](CONTRIBUTING.md) proof gate.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues