enola/Ruby

Ruby on Rails

Rails already has conventions. Nothing enforces them.

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.

01 · Credibility first

It reads Rails the way Rails reads Rails.

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.

2:13, unedited, on mastodon/mastodon at 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.
02 · Your conventions, not ours

One command binds the rules Rails already implies.

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.

enola constraints init .
writes enola/constraints/recipes.yaml
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

Blocking

policies-only-answer · mailers-do-not-enqueue · serializers-do-not-render · view-components-do-not-enqueue · view-components-do-not-render-controllers

Advisory — reports, never fails

jobs-do-not-render · models-do-not-render · models-do-not-reach-helpers · services-do-not-reach-controllers · request-api-stays-in-controllers

Why some are advisory

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.

03 · Proof beside estimate

One edit. Four findings. Three confidences.

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.

FindingConfidenceOutcome
rails-strict/models-do-not-render1.00Fails the build
rails-conventions/models-do-not-render0.90Advisory — the same edge, the lenient recipe
rails-conventions/request-api-stays-in-controllers0.90Advisory
layers — model → controller0.80Inferred, 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.

04 · Laws in Ruby

A team that writes Ruby can write its laws in Ruby.

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/constraints/jobs.rb
parsed, never executed
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.

05 · Beyond the parser

Ruby hides things a parser cannot see. So it asks something else.

Rubydex

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.

RBS & Sorbet

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.

Runtime

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.

06 · Start

Three commands.

a Rails application
binary, or Bundler
$ 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.