ArchSpec: Executable Architecture Specs for Ruby and Rails
Search for Blog
Ruby
Rails
AI
Agents
Open Source
ArchSpec
Architecture
ArchSpec 1.0: Executable Architecture Specification for Ruby's Agentic Coding Era
Carmine Paolino
Aug 20, 2026
More and more code is written by a model. Tests still tell you it works. RuboCop still tells you it’s tidy. Nothing tells you it still follows your architecture.
I released ArchSpec 1.0 today. It’s an architecture linter for Ruby and Rails. You declare your components and boundaries in one file, and every change gets checked, whether a person or an agent wrote it.
This is part of my push towards making Ruby one of the best languages to build with AI. RubyLLM is one piece. Schematist is another. Making the default Rails job queue fiber-based is another.
People and AIs take shortcuts
An agent or a person that’s in a hurry or doesn’t fully understand your architecture takes shortcuts.
The shortcuts work. They pass tests, implement features, and cross your boundaries. Months later, you realise your beautifully crafted architecture is now a patched mess.
Or, perhaps, you’re starting a new project and you want to ensure that the agent is using a good architecture, without having to police it at every step.
So you write it down in prompts and AGENTS.md. It helps, but it gets buried in its context window and something slips. Again, then again.
RuboCop reads your code and enforces a style. Herb does it for templates. Nothing did it for architecture. Until now.
ArchSpec: your architecture in one file
Declare your components and their rules in an Archspec.rb at the root of the project:
component :models, in: "app/models/**/*.rb"<br>component :controllers, in: "app/controllers/**/*.rb"<br>component :services, in: "app/services/**/*.rb"
models.cannot_use :controllers<br>services.cannot_call :render, :redirect_to, receiver: :none<br>controllers.can_only_use :models, :services
Then archspec check verifies every change. Models can’t reach into controllers. Domain code can’t touch adapters. Query objects can’t call save!. A pack exposes a public API and keeps everything else private. A directory has to stay empty, and says why.
If you’d rather not write rules at all, start from a preset:
architecture :vanilla_rails
That one line is the 37signals playbook: rich models, no service objects, no form objects, no policy objects, and app/services fails the build with a reason if anything shows up in it. There are presets for Rails, layered, hexagonal, clean architecture, modular monoliths, CQRS, and event-driven too.
Static Analysis, Not AI
I’m the author of RubyLLM so you’d think this uses AI.
Nope. ArchSpec doesn’t use AI.
Here’s how it works: Prism parses your Ruby, then ArchSpec extracts facts, references, inheritance, mixins, calls, definitions, and evaluates your rules against them. It’s deterministic, it’s offline, and it’s fast enough that you’ll leave it on: the full Discourse app, 1,899 files, was checked in 2.5 seconds, without booting the app.
Prism is its only runtime dependency. No Rails, no ActiveSupport, nothing else, so it works on any Ruby codebase. RubyLLM is a plain gem and it’s been the main proving ground since June.
It also won’t guess. ArchSpec doesn’t try to infer the “true” design pattern of arbitrary Ruby. You describe the architecture you want, and it tells you whether the code still matches. The AI is on the other side of the loop, writing the code that gets checked.
Failures an Agent Can Act On
When a rule breaks, you get this:
[error] models must not depend on controllers [dependencies.forbid]
app/models/user.rb:3:5
2 │ def admin_path<br>→ 3 │ UsersController.admin_path_for(self)<br>│ ^~~~~~~~~~~~~~~<br>4 │ end
note: User references UsersController
1 architecture violation found.
The format is a deliberate homage to clang and to Herb. Exact location, the offending span underlined, the evidence as a note, the rule id in brackets so you can suppress it narrowly.
A human reads it at a glance. An agent gets everything it needs to fix its own mistake without asking you: the file, the line, the rule, and why.
What It Caught in RubyLLM
I added ArchSpec checks to RubyLLM 2 months ago, in CI and as a pre-commit hook, and it has been instrumental in the big Protocol/Provider separation that’s coming in RubyLLM 2.0.
A protocol is a wire format, like Chat Completions, Responses, Anthropic’s Messages API, Gemini, or Bedrock Converse. A provider is an account you can talk to, like OpenAI, Azure, DeepSeek, or Ollama.
DeepSeek speaks Chat Completions. VertexAI speaks four: Gemini, Anthropic, Mistral, and Chat Completions. Writing each Protocol once is the reason the gem supports as many providers as it does.
That distinction is easy to state and easy to erode. The shortcut is to put a piece of wire format inside the provider that needs it, because right now that’s the only provider that needs it. Not on my watch:
providers.cannot_reference_constants...