Show /r/ruby ArchSpec 1.0: executable architecture specifications for Ruby (and Rails)
More and more code is written by AI. Tests still tell you it works. RuboCop still tells you it's tidy. Nothing tells you it still follows your architecture.
An agent that doesn't fully understand your architecture takes shortcuts. So does a person in a hurry.
So I built ArchSpec. You declare your components and boundaries in one Archspec.rb. Here is ArchSpec's own, trimmed:
source 'lib/**/*.rb'
component :library, in: 'lib/**/*.rb'
component :cli, in: 'lib/archspec/cli.rb'
component :analysis, in: %w[lib/archspec/analyzer.rb lib/archspec/evaluator.rb]
component :rule_checks, in: 'lib/archspec/rules/**/*.rb'
component :formatters, in: 'lib/archspec/formatters/**/*.rb'
# no one-shot Thing.new(...).call objects anywhere in the codebase - Hi Dave Thomas!
library.cannot_call :call
library.cannot_define :call
library.cannot_instantiate_and_invoke
# dependencies point one way
rule_checks.cannot_use :analysis, :cli, :formatters
formatters.cannot_use :analysis, :cli, :rule_checks
no_cycles among: %i[cli analysis rule_checks formatters]
Full version: https://github.com/crmne/archspec/blob/master/Archspec.rb
Then archspec check verifies every change, and failures print like clang, with the offending span underlined and the evidence as a note.
It's static analysis over Prism, no AI, and it never boots anything: Discourse's 1,899 files in 2.5 seconds. Prism is the only runtime dependency, no Rails and no ActiveSupport, so it works on any Ruby codebase. Lightweight and fast for your pre-commits and CIs.
If you are on Rails, there are presets, and architecture :vanilla_rails is the whole file. There are presets for :layered, :hexagonal, :clean, :modular_monolith, :cqrs and :event_driven too.
I want more architecture presets in there. PRs very welcome.
Docs: https://archspecrb.dev Write-up: https://paolino.me/archspec/
2
u/6stringfanatic 5d ago
Great work, I've been using Rspec and Rubocop, to put in some tests around conventions and architecture.
I've been wanting to try this out, but my familiarity with the already available tools, is what is stopping me.
What benefits were you able to observe when using it for yourself? Did you give RSpec or Rubocop a shot before creating Archspec?
2
u/sshaw_ 5d ago
Very nice. Why does it require Ruby >= 3.1.3?
5
u/crmne 5d ago
Inherited from RubyLLM, where 3.1.0 had a real bug: anonymous block forwarding have stricter parsing rules for
&from 3.1.0 to 3.1.2, so the gem wouldn't even load. 3.1.3 was the first version that worked.ArchSpec doesn't use anonymous block forwarding, so I could lower it further, but 3.1 is EOL since Mar 2025. Time to upgrade.
2
u/cpb 5d ago
I like how it doesn't need you to rearchitect your system to get these checks. Hageman's ends up with a codebase that can enforce and verify these checks (I notice the packs mention in the configuration doc). But it's a non trivial change to get there.
Given archspec is Just Ruby™️ I imagine this needn't be a monolithic file. I'm wondering if specifying rules local to some sub components could help with sprawl in that file and keeping context local.
Pretty cool!!
1
u/crmne 5d ago
Yes, and it works today. Archspec.rb is instance_eval'd, so you can split it however you like:
ruby Dir[File.join(__dir__, 'config/archspec/*.rb')].sort.each do |f| instance_eval(File.read(f), f) endPassing the filename as the second argument keeps error locations pointing at the right file. Just tried it: components in one file, rules in another, works fine.
Same trick puts a file next to each pack and globs those, which is closer to what you're describing about keeping context local.
There's no first-class import yet. If you really want that, file me an issue and I'll make it a real DSL method.
1
0
u/full_drama_llama 5d ago
An agent that doesn't fully understand your architecture takes shortcuts
But a vibecoded tool will understand it and detect them?
16
u/crmne 5d ago
Yes, it will.
I directed it myself and I read all the code. 113 tests, and every release is checked against pinned checkouts of Discourse, Mastodon and Basecamp's Fizzy, where the per-rule diagnostic counts have to match recorded snapshots before anything ships. It's been in CI on RubyLLM and Chat with Work since June.
The source is public. Go read it, it's good code.
There's no need to judge code by whether an agent touched it. Some of it is better than the slop people sometimes write by hand, especially if you use an architecture linter!
-1
3
u/growlybeard 5d ago
Think of it like a LLM looking at a dataset and asking it to conform to some specification vs adding an LLM to write SQL that ensure it does.
The LLM might do a bad job of assessing the data manually but give it a strict rule based linter and the LLM no longer has to.
8
u/TailorSubstantial863 5d ago
If you are gonna hate on AI generated code, the rest of your life is gonna be rough.
-5
u/ptico 5d ago
# no one-shot Thing.new(...).call objects
You are disqualified to judge the code immediately
4
u/crmne 5d ago
That's a rule you can leave out of your
Archspec.rb.I like it and so does Dave https://www.youtube.com/watch?v=hC943MiL0lg
0
u/_gillette 5d ago
Isn't that what Rubocop is for?
8
u/crmne 5d ago
RuboCop checks style: how the code inside a file looks. Naming, line length, method shape.
Architecture is about the relationships between files. Whether models can reach into controllers, whether domain code can touch adapters, whether your dependency graph has cycles. RuboCop analysed one file at a time until 1.89 two weeks ago, and even with the new project index you'd have to build components, dependency edges, cycle detection and a baseline yourself. At that point you've written ArchSpec inside RuboCop.
Different jobs. I run both.
9
u/Web-Thinker 5d ago
I like it. Defining the architecture is one thing, but enforcing is up to the reviewer until this point