intertwingly

It’s just data

Generated Rails


一二三 (hi-fu-mi)

Hifumi generates a complete Rails application from a natural-language prompt. It runs a plain rails new, then a planning model breaks your request into revisions and Claude agents write each one. The prompt steers them toward a default Rails 8 app: Tailwind and Hotwire, the default Gemfile, and sign-in via has_secure_password plus sessions rather than Devise. Each revision has to pass a verify loop before it's committed: bundle check, db:prepare, zeitwerk:check, a smoke test that requests every static GET page, and rails test. What you get at the end is an ordinary Rails repository, yours to keep.

Roundhouse reads a Rails app and compiles it, to Ruby, to native code via Spinel, and to a dozen other targets. It also has a check command that reports what it can't compile.

Generated apps are an interesting thing to point a compiler at. They are recent Rails, written the way the guides say to write it, and there can be a lot of them. Hand-written apps drift away from the defaults over the years; generated ones stay close to them. So I spent a day on what happens when you put the two together.

The bug Hifumi already caught

Hifumi's notes describe a project, an Event RSVP app, that crashed in the preview with undefined method 'authenticate_user!' for an instance of EventsController. The agent had reached for Devise's filter without Devise being in the Gemfile. Rails loads the class happily and answers every guarded page with a 500.

Roundhouse's check said nothing about it, and that was worse than it sounds. The compiler, finding no method to call, dropped the filter. The compiled app didn't crash where Rails did; it ran the action with no guard in front of it.

check now reports it as an error:

app/controllers/events_controller.rb:2:17: error[undefined_filter_target]:
  `before_action :authenticate_user!` names a method nothing defines;
  Rails raises NoMethodError on every action it guards

It looks in the controller, its ancestors, the concerns they include, and the methods Rails itself provides (including Devise's, when a model declares devise). It stays quiet when it can't see everything: a superclass from a gem, an include of a module the app doesn't define. Run against Mastodon, Lobsters, Campfire and Discourse, it reports nothing, which is the point: an error here has to mean something.

But Hifumi had already closed this one on its own. Its route smoke check was built after that project, and it catches the crash by requesting /events/new. What a static check adds is the part a smoke test can't reach. A filter written only: %i[edit update destroy] guards no page you can request without an id, so the smoke test never sees it. check sees it regardless, and runs in a fraction of a second.

The part that wasn't easy

The real finding was elsewhere. I don't have any of Hifumi's output, so I built a stand-in: a fresh Rails 8 app with two scaffolds and the sign-in Hifumi's prompt asks for, produced the quickest way Rails offers, bin/rails g authentication. Whether Hifumi's agents run that generator or write the same thing by hand, I don't know. check reported zero errors. Compiling it with Spinel produced four refusals, and nothing was written.

One was the app's own fault: the generated Authentication concern calls root_url, and the scaffolds don't define a root route. Rails would raise there too. The other three were Roundhouse's:

The analyzer knew the types of both. Nothing implemented them. A clean check over a program that cannot build is the shape of mistake I care most about, because check being quiet is supposed to mean the output works.

Fixing those two got the app to build. Then I ran the tests the generator writes, and they found the rest. Each item below is something the generated app relies on and the compiled app didn't do:

With those in place, the generated tests pass on both CRuby and the Spinel-compiled binary: passwords 7 of 7, user 1 of 1, sessions 3 of 4. The fourth fails on a known difference: a cookie that was never set reads as "" rather than nil. Campfire's own suite still passes, 406 of 406.

None of this was exotic, and none of it is about Hifumi. Every Rails 8 app that runs the authentication generator has all of it, however the app was written. I'd been testing against Campfire, which predates the generator and wrote its own sign-in. Rails' own default code found in an afternoon what months of a hand-written app hadn't, which is the argument for testing a compiler against defaults.

What isn't true yet

What it could become

What I'd like to see is Hifumi offering a compiled version of what it generates, optionally or by default, locally and in the Dockerfile. Looking at how Hifumi works, there are three places a compiler could fit, and they want different things.

In the verify loop: check, not compilation. roundhouse check takes a fraction of a second and reports file and line, which is the form Hifumi already hands to its fix agent when a check fails. It would sit next to route smoke, advisory. Its findings come in two kinds, and they need to go to different places. A filter naming nothing, or a root_url with no root route, is a bug in the app; that goes to the agent, as a route smoke failure does. "Roundhouse can't compile this yet" is my problem, not the app's; that goes to a ledger, and to me, and never to the agent. An agent that spends its fix attempts rewriting a working app around a compiler's gaps is worse than no check at all. Hifumi's notes record rejecting a linter on the same grounds: its errors were style, not breakage. That's the right bar, and the one this has to meet.

Not in the preview, at least at first. Hifumi rebuilds a preview container for every revision and runs it in development mode, inside an eight-minute build budget. That's the right design while someone is still iterating. Compiling takes minutes and brings a compiler toolchain into the image, and buys little while the app is still changing. If a compiled preview ever earns a place, it's as a separate action on a revision you've accepted.

At export and deploy: a second Dockerfile. This is where it fits naturally. The repository you take away keeps the Dockerfile that rails new wrote. Beside it would sit a second one that compiles the app in a build stage and ships just the binary and SQLite, plus a local bin/compile that does the same without Docker. Roundhouse already builds this kind of image for Campfire, so this is packaging, not invention. Kamal deploys either. It's offered only when check reports no errors, so "optional" stays honest, and the Rails repository remains the thing you own.

Roundhouse also translates an app's own tests, so the export could run them twice, once against Rails and once against the compiled app. A difference there is a bug in Roundhouse, and the report should come to me, not land on the person who asked for an app.

Should we do this?

That's a question for Paweł Strzałkowski more than for me. He built Hifumi, and he knows what its users run into. The questions I'd like answered:

And for anyone else generating Rails apps, by agent or by template: would you run a check that tells you, before anyone opens the page, that a filter names a method nothing defines?

Paweł and I will both be at Deccan Queen on Rails in Pune next week. If you're there too, find either of us.


Roundhouse is open source: dual-licensed MIT / Apache-2.0. The changes described here are commits 65cc85c1 and 16ebe6a9; the Rails 8 authentication generator's output, overlaid on a blog and run, is pinned in tests/rails8_authentication.rs.