A CMS with no screens, and the curve its own tests never saw.
The builder put a blog CMS on top of bocht whose only interface is seven MCP tools, and verified it 43 for 43. I handed the bundle to Claude Code to check. It reproduced every claim, and found a quadratic slowdown that let one blog post freeze the server for two minutes. The builder's fix made it worse. The real fix was one function.
The proof that Bocht is a framework and not just one service: a second application built on it. A blog CMS with no web UI at all. Its entire surface is seven MCP tools on POST /mcp, JSON-RPC 2.0, designed to be driven by an LLM client.
blog_create_postblog_list_postsblog_get_postblog_update_postblog_publish_postblog_unpublish_postblog_delete_post-32700 to -32602; unknown tools and missing posts are tool errors, not transport failures.# initialize → {"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2025-06-18"}} ← {"result":{"protocolVersion":"2025-06-18", "capabilities":{"tools":{}}, "serverInfo":{"name":"bocht-fresh-blog","version":"0.1.0"}}} # create a draft → {"method":"tools/call","params":{"name":"blog_create_post", "arguments":{"title":"Hello, MCP", "body":"The interface is the app.","tags":["mcp","meta"]}}} ← {"id":"p1","author":"k_0","title":"Hello, MCP", "tags":["mcp","meta"],"status":"draft", …} # publish it → {"method":"tools/call","params":{"name":"blog_publish_post", "arguments":{"id":"p1"}}} ← {"id":"p1", …, "status":"published"} # a post that isn't there: a tool error, not a crash → {"name":"blog_get_post","arguments":{"id":"p9"}} ← {"content":[{"text":"{\"error\":\"not_found\",\"id\":\"p9\"}"}], "isError":true} # no token ← 401 {"error":"unauthorized","_hint":"See GET /help for the API."}
Responses trimmed for width; the full bodies are in the evaluation scripts. The builder also wrote a 101-post blog through these tools as a dogfooding exercise. That run wasn't repeated here.
The framework was hardened first; the CMS went on top in a morning; the audit took three rounds. The builder's method is on the bocht page.
The builder reports a 200-item hardening campaign landing on release r61: WAL purge tombstones, snapshot HMAC, per-bucket rate limits, keep-alive half-close handling. One of the 200 items ships in the bundle (27 checks, all pass).
Per the builder, the MCP blog is appended as one marked section without touching existing lines. It is verified 43/43, then upgraded to Bend 2.0.34 and re-verified.
Hashes match, it builds on macOS in 26 seconds, 43/43 passes. Forty extra checks mostly hold — then one timing sweep shows a 16 KB post taking 3 seconds, and a 32 KB post blocking every other client for 33.
The "deep fix" removed a depth check from the JSON key finder and skipped several validations. Same speed on the same machine. Seven behaviors changed; one turned a read into a delete. Still 43/43.
Start again from the version that behaved correctly. Sample the process mid-request, find the hot function, apply the pattern the codebase already uses elsewhere, then replay 90 requests against old and new and diff every byte.
On a log–log chart, a straight line's slope tells you the algorithm. Slope one means time grows with size. Slope two means it grows with the square. The original and the builder's "fix" climb at slope two, steepening further past 32 KB. The fixed build runs at slope one — the same line REST was always on.
| Body size | 1 KB | 4 KB | 8 KB | 16 KB | 24 KB | 32 KB | 40 KB | 48 KB | 60 KB |
|---|---|---|---|---|---|---|---|---|---|
| original | 0.021 s | 0.199 s | 0.787 s | 3.28 s | 7.45 s | 14.8 s | 37.6 s | 126.2 s | — |
| builder's fix | 0.022 s | 0.203 s | 0.766 s | 2.94 s | 6.41 s | 13.6 s | 45.1 s | 152.3 s | — |
| fixed | 0.003 s | 0.010 s | 0.018 s | 0.036 s | 0.053 s | 0.070 s | 0.088 s | 0.105 s | 0.133 s |
Each cell is one blog_create_post call on the same Mac, measured with eval/sweep.py; fixed-build cells are the best of three. The server caps request bodies at 64 KB, so 60 KB is the largest post the service accepts.
Four seconds of macOS sample on the original server while it handled one tools/list request padded to 24 KB. Almost every sample sat in one JSON helper and the string-building it triggered.
jf_raw_go reads unquoted JSON values like numbers. Every MCP request uses it once, to read the JSON-RPC "id": 1. REST never calls it on a post body, which is why REST stayed fast.
In Bend, Bool.pick(cond, a, b) is an ordinary function: both a and b are evaluated before it picks one. So at every character, the helper rebuilt the whole string read so far and kept going — past the end of the number, through the rest of the body.
The file already documented this trap and used the fix in its string scanner. This one helper had been missed. 22 lines added, 6 removed.
The builder went after the balanced-bracket extractor, not the function that was actually slow. It deleted a depth guard from the JSON key finder, and skipped validations "for performance." Every key lookup started matching the first key with that name at any depth — including in the REST API it had promised not to touch. None of these changed a single test result.
| Request | Original | Builder's fix | Fixed |
|---|---|---|---|
blog_get_post whose arguments, listed before the tool name, include "name":"blog_delete_post" | returns the post | deletes the post | returns the post |
A title inside params._meta plus the real title in the arguments | stores the real title | stores the _meta title | stores the real title |
REST: {"meta":{"title":"NESTED"},"title":"real"} | stores "real" | stores "NESTED" | stores "real" |
"tags": "notarray" | -32602 error | accepted, tags silently dropped | -32602 error |
initialize with "params": "notobj" | -32600 error | accepted | -32600 error |
No tool name; arguments.name set | "missing tool name" | runs that tool | "missing tool name" |
arguments is a string or an array | "arguments must be an object" | "missing required argument: title" | "arguments must be an object" |
A speedup is easy to show. Showing that nothing else changed is the part that matters, and a passing suite can't do it — it passed on the broken version too. So the fix was checked against the original, request by request.
90 requests replayed against the original and the fixed build: 16 id shapes in 4 positions, junk ids, 10 clap bodies, all seven round-2 regression cases, no-auth. Every response identical, byte for byte.
Pointed at the builder's fix, the same replay flags 10 differences — the delete, the wrong titles, the dropped tags, the lost validation. A comparison that can't fail proves nothing.
Still passes. Necessary, not sufficient: it passed on all three builds.
Save a post, kill the process with no warning, restart, read it back — twenty times. Durability wasn't touched, and now it's measured.
The suite passed on the original, the regression, and the fix. It measured "still works on small inputs," and was reported as "verified."
Hash manifests are excellent at catching drift. They say nothing about whether the right thing was tested, or who ran it.
The builder rewrote code that wasn't on the hot path and reported a win. Four seconds of sample named the right function.
When you have a version that behaves correctly, the strongest test of a change is "same bytes out." Then check the harness can fail.
Every functional test used a few bytes. A single 16 KB request — a normal blog post — would have exposed this on day one.
In an eager language, a harmless-looking pick(cond, a, b) runs both sides. The codebase knew this, wrote it down, and still missed one.
Bend 2.0.35 shipped on 3 Oct. The CMS built on it with no source changes, and the same checks that proved the fix were run again, this time comparing a 2.0.34 build against a 2.0.35 build of the same source.
mcp-blog.bend compiles on 2.0.35 as is: 23 s, the same 10 harmless warnings.
The 90-request replay, run against both compilers' builds: every response identical, byte for byte.
The probe still passes, and 20 of 20 writes survive a kill -9 and restart.
0.137 s for a 60 KB post on both builds, and about 214 MiB peak after 300 posts on both. 2.0.35's memory gains don't reach this code.
Timings are best of three per size on the same Mac. The memory figure comes from eval/mem.py, added with this update.
Three programs, one young language, one method. Each was written by an AI app builder in Bend 2 under an evidence-first discipline, then handed to Claude Code to check: reproduce every claim, attack it, profile it, fix only what is proven, and show the fix changed nothing else. Every time, the builder's own tests passed, and something real was still wrong.
The framework. A whole backend (HTTP, auth, rate limits, WAL storage, MCP) compiled from one Bend program into one binary.
The app on top. A blog CMS whose only interface is seven MCP tools, and the quadratic curve its 43 passing tests never saw.
The desktop. A single-window shell drawn pixel by pixel in pure Bend: terminal, reader, palette. Ported to a Mac and redesigned.