Ruby on Rails
Ruby has no compiler, no import graph, and no find-usages you can trust. enola. parses your application into a graph, grades a change against the graph you had before it, and enforces the conventions you declare — each carrying the reason it exists. It arrives holding no opinion about your layers.
Before a tool is allowed an opinion about your architecture, it should prove it understands the framework. resources :invites, only: [:index, :create, :destroy] is three routes, not seven. A singular resource nested in a plural one hangs off the parent member and has no :id of its own. Engines, mounted plugins and draw(:pkg) files are route files too.
And where the convention needs ActiveSupport’s irregular inflections to resolve — resource :person is served by PeopleController — it emits no handler at all rather than a confident wrong one.
348a6689, enola 0.4.4. Three routes from only:, six from except: with no filters#show. Then rails-mvc recognised at 86% with seven layer violations — a controller concern mixed into models, services, a worker and a rake task. Declare the layer order in eight lines and the same seven return at confidence 1.00.enola ships convention sets as recipes — named bundles of laws, each carrying its reason. enola constraints init binds every recipe whose roles resolve to directories you actually have, names the ones it could not bind, and guesses nothing.
rails-conventions bound 8 role(s); optional, left for the author: services rails-strict bound 8 role(s); optional, left for the author: services, concerns ruby-conventions bound 1 role(s) layered bound 4 role(s) vanilla-rails not bound: no directory for services, forms, decorators, presenters ports-and-adapters not bound: no directory for ports, adapters
policies-only-answer · mailers-do-not-enqueue · serializers-do-not-render · view-components-do-not-enqueue · view-components-do-not-render-controllers
jobs-do-not-render · models-do-not-render · models-do-not-reach-helpers · services-do-not-reach-controllers · request-api-stays-in-controllers
A job or model that renders goes through ApplicationController.renderer — a sanctioned path that still reads as a crossing. Worth surfacing; not worth breaking a build over.
A plain rails new binds layered and ruby-conventions only: the Rails recipes need app/policies, app/serializers and app/components to resolve. That is the tool declining to invent structure you have not got.
A model rendering through the controller, on a fresh Rails 8 application. Every finding names the rule, the forbidden edge, and the line — and the confidence tells you which are proof and which are opinion.
| Finding | Confidence | Outcome |
|---|---|---|
rails-strict/models-do-not-render | 1.00 | Fails the build |
rails-conventions/models-do-not-render | 0.90 | Advisory — the same edge, the lenient recipe |
rails-conventions/request-api-stays-in-controllers | 0.90 | Advisory |
layers — model → controller | 0.80 | Inferred, never gates by default |
You declared the first. enola only recognised the last, by matching module paths against known taxonomies, so it is capped below 1.00 and fails nothing unless you name it. Declared law is proof. A recognised pattern is an estimate. The number says which you are looking at — and nothing fails without --fail-on.
Files ending .rb under enola/constraints/ are parsed with the same grammar the extractor uses and never executed, compiling to exactly what the YAML loader produces.
Enola.architecture "storefront" do rails law "background jobs never invoke controller code" do jobs.must_not_call controllers why "rendering from a job goes through ApplicationController.renderer" end end
rails declares the conventional parts from the directories Rails puts them in, so you write only what is yours. Nineteen verbs cover the 21 rule forms.
Constant references resolved through Ruby’s own nesting and inheritance rules, plus each class’s linearised ancestor chain. This is what the ancestor: selector reads.
Declared signatures arrive as claims: a measured symbol gains typed: true and names the file that said so. A claim about the implementation, never proof of it.
Observations from a booted application — the final route table, reflected associations — carried as runtime-observed, never as a static fact.
Every provider fact declares how it was resolved, so a consumer can always weigh a claim against an extraction. Hotwire, Packwerk, Grape, graphql-ruby, schema.rb and structure.sql are read by the extractor itself — see what enola extracts from Ruby.
$ enola constraints init . # bind the conventions you have $ enola baseline pin . # the "before" $ enola check --fail-on=constraints . # or, inside Bundler: $ bundle add enola-rb $ bin/rails generate enola:install $ bin/rake enola:check
The Ruby gems are third-party integrations maintained by Muhamed Isabegović, who also contributed the Ruby constraint DSL that ships in enola. Both Ruby fact providers are on by default there. The full workflow, with the output each step prints, is docs/RAILS.md.