carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1# 🤖 TodoMVC bakeoff on Carloku — design
2
3Status: draft for Paul's approval, 2026-10-23. Reviewed by Astra (builder check).
4
5## Why
6
7CARLOS apps don't have to be written in Go, and the CARLOS Platform runs
8any binary that keeps its process contract. This bakeoff shows that with
9working apps. It's driven by curiosity and acts as a showcase. It isn't
10meant to pick a winner or feed a decision.
11
12We build the same app, TodoMVC, in nine language setups. Each one renders
13HTML on the server and layers JavaScript on top, and all of them are
14deployed to the Carloku **bakeoff** account (`bfj`). A report page on the
15same account compares them.
16
17## Goals
18
19- Nine working TodoMVC apps, live on Carloku, one per setup:
20 1. Go, without Rastrillo
21 2. Go, with Rastrillo
22 3. Rust
23 4. Zig
24 5. OCaml
25 6. Haskell
26 7. Swift
27 8. Nim (replaced Mojo, Paul's call 2026-10-24: Mojo can't make a
28 single-file binary; see `docs/toolchains.md`)
29 9. Spinel (a Ruby compiler that produces native binaries)
30- Every app passes the same behaviour tests with JavaScript on and with
31 it off.
32- A report page that puts honest numbers side by side: what each build
33 cost in tokens, how readable the code is, how fast it responds, how it
34 holds up under load, and the other measures listed below.
35
36## Non-goals
37
38- Choosing a "best" language. The report presents numbers and doesn't
39 crown anyone.
40- Tuning one app beyond the others after the build. Every app gets the
41 same build process and the same review-and-fix round, and nothing more.
42- Rich features beyond TodoMVC: no sign-in, no sharing, no sync between
43 devices.
44
45## Anti-goals (failure even if everything technically works)
46
47- **A report that looks rigged.** One app gets a different UI, a different
48 database, a bigger instance, more tuning, extra review rounds, or quietly
49 drops part of the spec.
50- **An app missing from the report without comment.** If a setup can't be
51 built or can't run on Carloku, the report says so plainly and says why.
52- **Working around Carloku.** If the platform lacks something an app needs
53 (for example, a binary that needs files beside it), we file a request
54 against the platform and report the app as blocked on it. We don't
55 build workarounds. (Paul's standing rule.)
56
57## Decided with Paul (2026-10-23)
58
59- **One shared UI.** Every app uses TodoMVC's standard markup and CSS
60 (`tastejs/todomvc-app-template`, `todomvc-app-css`). For the bakeoff
61 this overrides the CARLOS skill's default that Rastrillo apps use
62 Rastrillo's own UI components. The Go + Rastrillo app uses the TodoMVC
63 CSS too.
64- **Server-rendered with progressive enhancement.** No client-side app
65 framework. The page is HTML from the server and still works with
66 JavaScript turned off.
67- **Each agent chooses its own stack.** Web server, router, templating,
68 SQLite binding and so on are each agent's own best recommendation for
69 its language.
70- **The builder is Opus 5.5 for every app.** Astra reviews every app
71 through the codex CLI, and the same Opus agent fixes what the review
72 raises.
73- **Spinel is already supported on Carloku.** The Spinel builder starts
74 from lpay's working setup (`paulca/lpay`, directory `spinel/`): tep
75 (a Sinatra-style layer that drives Spinel), the `tep-carlos.patch` for
76 the socket contract, and the zig-based cross-build to a static
77 aarch64-musl binary in `BUILD-arm64.md`. Mojo is expected to work if it
78 compiles to a single binary.
79- **Arm64 builds:** try docs-t1 first (cross-compiling or an emulated
80 arm64 container), and fall back to tito-studio over Tailscale.
81- **After the one fix round:** remaining correctness gaps ship as they
82 are, and the report shows them. Security findings get a second fix
83 round, counted separately, because the apps are public.
84- **Downloads approved:** GHC (via ghcup) and Mojo (via pixi) may be
85 installed under `~` on docs-t1.
86- **Everything lives on the bakeoff account** (`bfj`): nine apps and the
87 report page.
88
89## What every app must do (the shared brief)
90
91Each builder gets the same written brief. It contains:
92
93### Behaviour
94
95- The TodoMVC app spec (`tastejs/todomvc/app-spec.md`), with these changes
96 because the server holds the data:
97 - **Persistence:** in SQLite on the server, not in `localStorage`.
98 - **Routes:** real paths `/`, `/active` and `/completed`, not `#/`
99 fragments. The selected filter survives a reload because it's in the
100 URL.
101 - Editing state isn't persisted (same as the spec).
102- **With JavaScript off,** everything works through plain forms and links:
103 add, toggle, toggle all, edit, delete, clear completed, and the three
104 filters. Each action is a POST that redirects back to the current
105 filter (post/redirect/get). Editing without JS uses a link to an edit
106 view of that item, such as `/?edit=<id>`, rendered with the edit field
107 open.
108- **With JavaScript on,** the page behaves like the reference TodoMVC:
109 double-click to edit, Enter saves, Escape cancels, blur saves, an empty
110 title deletes, toggles happen without a full page reload, and the
111 counter and the "Clear completed" button update in place. The JS sends
112 the same POSTs and swaps in server-rendered HTML or applies a small
113 JSON reply (the builder chooses). There's no build step and no
114 framework. Plain ES modules are allowed, and so is vendored VanJS (the
115 one reactive library CARLOS sanctions).
116- **Who owns a list:** the whole app is one instance. Each visitor gets
117 their own list through an anonymous, random, `HttpOnly`, `SameSite=Lax`
118 cookie set on the first visit. There's no sign-in. Forms are protected
119 against cross-site posts (by SameSite plus an Origin check, or a CSRF
120 token; the builder chooses).
121
122### Platform contract
123
124- One binary for `linux-arm64` (the platform default; `carlos deploy
125 --target`), started as `<bin> --socket <path> --db <path>`. It listens
126 on the unix socket, has no `$PORT`, and serves `GET /healthz` and
127 `GET /api/version`.
128- **Linked statically, or with only libc.** Instances run sandboxed and
129 see only their own build, so the builder must not assume any shared
130 library exists on the box. `carlos vet --boot` (a local check that the
131 binary starts and answers `/healthz`) must pass before the first deploy.
132- Reads `CARLOS_HOST` when it needs its own hostname. Never parses the
133 socket path.
134- Uses SQLite in WAL mode. Migrations only ever add.
135- The app starts quickly after hibernation (sleeping when idle) and shuts
136 down cleanly on SIGTERM.
137- Configures the build probe (`carlos apps create --build-probe
138 /api/version`) so `carlos deploy` confirms that the new build is what's
139 actually serving.
140
141### Repo shape for each app
142
143- `apps/<slug>/` in this bakeoff repo. The slugs are `go`, `go-rastrillo`,
144 `rust`, `zig`, `ocaml`, `haskell`, `swift`, `mojo` and `spinel`.
145- `AGENTS.md` first. It holds the one rule and the tenancy declaration:
146 one instance, anonymous lists by cookie, trust class C (plaintext todo
147 text, chosen and written down).
148- A `README.md` with how to build, run locally, test and deploy. One
149 command for each (a `Makefile` or a script).
150- The app's own tests, whatever is normal for its language.
151- The Carloku app name is `todo-<slug>`, e.g. `todo-rust`.
152
153### What the builder may not do
154
155- Change the markup or the CSS beyond adding hooks the JS needs, such as
156 `data-` attributes or wrapping each toggle in a `<form>`. The rendered
157 page must look identical to the reference.
158- Add caching layers, a CDN or edge tricks that the other apps don't
159 have. Each app may set ordinary HTTP cache headers on its static
160 assets.
161- Spend effort on performance tuning beyond what an experienced developer
162 would write the first time. "Write it the way you'd normally write it
163 well" is the instruction.
164
165## How the apps get built
166
1671. **Shared tooling first, built by the orchestrator (this session)**
168 before any builder starts, so every app is judged by the same tools:
169 - The brief, as one file every builder reads.
170 - A **behaviour test suite** adapted from TodoMVC's Cypress tests
171 (`tastejs/todomvc/cypress`). It runs against any URL, once with JS
172 on and once with JS off. The JS-off run uses forms only and skips
173 the parts that are JS-only, such as double-click editing (replaced
174 by the edit link). Builders run it locally against their own app.
175 - A **load test script** and a **metrics collector** (see "Metrics").
1762. **Toolchains installed** per user, under `~` (`rustup`, Zig tarball,
177 `opam`, `ghcup`, Swift's static Linux SDK, Mojo through `pixi`,
178 Spinel built from source). GHC and Mojo each take several GB, so we
179 ask Paul before downloading them (machine rule).
1803. **Nine Opus 5.5 builders run in parallel**, each in its own headless
181 `claude` session with JSON output so token use is recorded. Each
182 starts from a fresh context with the same brief and the CARLOS skills
183 (`carlosframework/skills`), plus Rastrillo's own `SKILL.md` for
184 Go + Rastrillo. Each builds, tests locally, then deploys to `bfj` with
185 `carlos deploy` and checks it on the live URL.
1864. **Astra reviews each app** through `codex exec` against the brief, the
187 spec and the code. The review is limited to correctness, spec
188 compliance and security. It doesn't ask for performance tuning.
1895. **One fix round.** The same Opus builder session fixes what Astra
190 raised, then redeploys. Only one round, for every app. The one
191 exception is security: if Astra still finds a security problem after
192 that round, there's a second round for security fixes only, with its
193 tokens reported separately.
1946. **Blind readability scoring by Fable.** Each app's code is shown with
195 the language named but nothing else identifying, and scored against a
196 fixed rubric. Astra doesn't score readability because it already
197 reviewed the code.
1987. **Measurement and report** (below).
199
200All local servers bind to `127.0.0.1`. Scratch space goes in `$TMPDIR`
201and is removed at the end.
202
203## Metrics
204
205All of them are collected by scripts in this repo and can be re-run.
206
207| Measure | How |
208|---|---|
209| Tokens to build | From each builder's session log: input, output, cache read and cache write, plus turns and wall-clock time. The first build and the fix round are reported separately, and so is the Astra review. |
210| Correctness | Behaviour tests passed, JS on and JS off, against the live URL. Spec gaps found by review. |
211| Readability | Fable's rubric score (naming, structure, how easy it is to follow one request end to end, error handling, comments), with a short written justification. |
212| Size of the code | Lines of code split three ways: server code, templates, and JS. Also the number of third-party dependencies. |
213| Build | Three build times for the arm64 binary that ships: **cold** (the app and every third-party library compiled from already-downloaded sources; compiler and standard library as shipped), **warm** (libraries already compiled, app rebuilt from clean), and **one-line change** (rebuild after a trivial edit in the request-handling code). Plus binary size. Paul asked for cold and warm, 2026-10-23. Measured on docs-t1 where the app cross-compiles there; otherwise on the machine that built it, with the machine named beside the number and kept out of the ranking. |
214| Response time | Time to the first byte for `GET /` with an empty list and with 50 items, from docs-t1, median of repeated runs. |
215| Wake time | Time to the first byte on the first request after the instance has gone to sleep. |
216| Under load | A ramp for reads (`GET /` on a 50-item list) and for writes (toggling an item): 1, 2, 4 … 1,024 concurrent connections, 10 s per step. Reports peak requests per second, latency at every step, and **where it slows down**: the most connections at which doubling the load still raised throughput by 10% or more. A ramp stops early after two flat doublings or more than 1% errors. Paul asked for this, 2026-10-23, replacing fixed loads at 1, 16 and 64. |
217| Memory | Peak and idle memory from the platform, if it exposes them. If it doesn't, we file a request against the platform rather than measure inside the app. |
218| Page weight | Bytes of HTML and JS sent for `GET /`. |
219
220Load tests run from docs-t1 over the public internet, so the numbers
221include network time. That's the same for every app, and the report says
222so. Every app runs on the same box class and instance size, and we run
223the apps one after another, never at the same time.
224
225## The report page
226
227- A static site on `bfj` (`carlos deploy --kind static`), named
228 `bakeoff`.
229- One page with a summary table of every measure for every app, and a
230 chart per performance measure. Each app gets a short section covering
231 its stack, notable choices, Astra's main findings, Fable's readability
232 notes, a link to the live app and a link to its source.
233- A "How we measured" section and a "What this does and doesn't show"
234 section, including the network caveat, Spinel's single-threaded server
235 and any app that's blocked.
236- Built from the metrics files by a script, so a re-run regenerates it.
237- Report copy goes through the copy-review gate before it ships.
238
239## Known risks
240
241- **Everything targets `linux-arm64`, and docs-t1 is x86-64.** Rust, Zig,
242 Go and Swift (static Linux SDK) cross-compile well, and so does Spinel
243 (lpay's zig recipe). OCaml, Haskell and Mojo are harder. Each tries
244 docs-t1 first, by cross-compiling or in an emulated arm64 container,
245 and falls back to tito-studio. If neither works within the effort
246 limit, that app is reported as blocked.
247- **Mojo** may need runtime files beside the binary. The binary kind takes
248 a single file. If Mojo can't produce one, we file a platform request and
249 report Mojo as blocked.
250- **Spinel** has no threads. lpay's notes flag its aarch64 fiber context
251 switch as untested at runtime. Whatever concurrency model the Spinel
252 server ends up with, the report states it next to the load numbers.
253- **Load testing shared boxes** can affect other people's apps on the same
254 box. We keep load runs short, one app at a time.
255
256## Effort limit
257
258Every builder gets the same limit. If an app still doesn't have a local
259build passing the behaviour tests after **4 hours of wall-clock time**,
260the builder stops and writes down where it got stuck, and the app is
261reported as blocked. All tokens a builder spends are reported, including
262for blocked apps and time spent getting toolchains working.
263
264## Left to the builders
265
266- Each app's libraries, framework, server, templating and SQLite binding.
267- How the JS talks to the server (HTML fragments or small JSON).
268- Internal code layout, within the repo shape above.
269
270## Calls made by the orchestrator, not by Paul
271
272- Real paths instead of `#/` routes, and per-visitor lists by anonymous
273 cookie.
274- Fable for blind readability scoring, separate from Astra's review.
275- One review-and-fix round per app.
276- All apps in one repo under `apps/<slug>/`.
277- App names `todo-<slug>`, and the report app named `bakeoff`.
278- The shared test suite and load test are written once by the
279 orchestrator, not by each builder.
280- The effort limit: 4 hours of wall-clock time per builder.
281- Build time for apps that can't cross-compile on docs-t1 is measured
282 where they're built, with the machine named, and isn't ranked against
283 the others.
284