Describe a software system and boxr interviews you until twelve core requirements are known, draws the components, connections and data flows, checks its own drawing, and exports a build prompt a coding model can implement.
Solo build
Ask a chat model to design a system and you get a confident wall of boxes. Some of them connect to nothing, a database gets drawn that nothing ever writes to, and half the requirements behind it were guessed rather than asked. It looks finished, which makes it worse than a blank page.
I wanted a planner that behaves like a good senior engineer at a whiteboard: asks what it doesn't know, draws in front of you, checks its own work, and hands off something a coding agent can actually build from.
The model never draws directly. It streams typed edits ("add node", "connect") against a strict JSON schema, and each batch of edits applies completely or not at all. An edit that names a box that doesn't exist yet waits in a queue and is settled at the end of the turn, so a model that writes connections before components doesn't corrupt the document.
Drawing happens in passes. First every layer and component, with a checklist of parts the description implies. Then connections and key flows, split into two parallel calls on two Groq keys once there are six or more components. Then one edit adds any checklist item still missing.
Before any of that, a small fast model interviews you against twelve requirement slots. Every filled slot has to quote your own words, and plain code drops any that don't, so a guessed requirement turns into a question instead of a silent assumption.
After drawing, plain code counts problems (unconnected boxes, stores nothing writes, queues nothing reads) and fixes the ones with one right answer. Anything left goes to up to three AI repair passes, and each pass is kept only if the problem count actually drops. The whole app runs in the browser with no backend, routing across seven providers with per-role model preferences and timeouts.
description
│
├─▶ 1. INTERVIEW 12 requirement slots
│ │ a slot stays filled only if it quotes you
│ ▼
├─▶ 2. PLAN optional, written as prose
│ ▼
├─▶ 3. SKETCH typed JSON edits, never raw drawing
│ │ outline → connect (2 calls, 2 keys) → fill gaps
│ ▼
├─▶ 4. GUARDRAILS each batch applies all or nothing
│ │ edits naming unbuilt boxes wait in a queue
│ ▼
├─▶ 5. CHECKS + REPAIR code fixes first, then up to 3 AI passes
│ ▼ a pass is kept only if problems drop
├─▶ 6. LAYOUT ELK positions, React Flow draws, IndexedDB saves
│ ▼
└─▶ 7. HANDOFF build pack for Claude Code, Codex, CursorAgainst a plain prompt on the same model, across 14 paired briefs, boxr drew 14 problem-free diagrams to the plain prompt's 4 (McNemar p = 0.002). Measured against a hidden spec with a simulated user answering only from it, drawings held 66% of the parts only the spec implies, against 6% from the one-line brief alone.
The exported build pack answered 62 of 64 comprehension questions across four models, against 54 for the Mermaid export, while using 28% fewer tokens than the JSON export.
It is not better on every axis, and the eval says so: boxr's drawings are smaller (8.5 components against 10.6), it takes 7.1 seconds against 1.5, and it spends 2.7 times the tokens. Expected-part coverage was 76% against 82%, not a significant difference.
The interview eval found that every miss had the same cause: the interview model filled a requirement by guessing instead of asking. The fix wasn't a better prompt. It was a rule enforced in code, that a slot only counts if it quotes the user.
Running a paired evaluation against the plain-prompt baseline kept me honest. It confirmed the guardrails work, and it also showed the costs (smaller drawings, slower, more tokens) that I would never have noticed by eyeballing my own demos.
/**
* Validate and apply a whole batch. The live doc is never touched until every op has
* succeeded against a draft. That is the atomicity guarantee.
*/
export function applyBatch(doc: Doc, batch: Batch): { doc: Doc; outcome: Outcome } {
const draft = clone(doc);
const missing = new Set<NodeId>();
const errors: string[] = [];
for (const op of batch.ops) {
const problem = applyOne(draft, op);
if (!problem) continue;
if (problem.error) errors.push(problem.error);
for (const id of problem.missing ?? []) missing.add(id);
}
// Structural errors are never retryable. Check them first: a batch that is both
// malformed and forward-referencing should be rejected, not parked in the queue forever.
if (errors.length) return { doc, outcome: { status: 'rejected', errors } };
if (missing.size) return { doc, outcome: { status: 'queued', missing: [...missing] } };
return { doc: draft, outcome: { status: 'applied' } };
}