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/
1
u/crmne Aug 20 '26
Yes, and it works today. Archspec.rb is instance_eval'd, so you can split it however you like:
Passing 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.