Configuration
Archspec.rb is Ruby.
Project
source "app/**/*.rb", "lib/**/*.rb"
ignore "app/legacy/**/*.rb"
todo "archspec_todo.yml"
ArchSpec analyzes .rb, .rake, and .erb files matched by source or component
patterns. The default source patterns select .rb files; opt into Rake tasks
and ERB templates with source patterns such as
source "app/**/*.{rb,erb}", "lib/**/*.{rb,rake}" or components such as
component :tasks, in: "lib/tasks/**/*.rake" and
component :views, in: "app/views/**/*.erb". Ignore patterns apply to all three
file types.
Note that broad source or component patterns, such as lib/tasks/**/*, include
all supported formats: .rb, .rake and .erb files.
For ERB templates, Herb provides a complete Prism program with template source locations, which ArchSpec analyzes so diagnostics point to the original template. ArchSpec checks the Ruby code, not the HTML structure. See How It Works for the parsing pipeline.
Todo ids are computed from the rule, path, message, and evidence, not the line number, so entries survive edits that shift code.
Opt into runtime facts with reflect, which names the producers
archspec reflect runs:
reflect :rails
reflect :widgets, require: "./lib/archspec/widgets_facts"
facts "config/archspec_facts" # optional; the default directory is archspec_facts
Add auto: true to let check and explain run that producer themselves
when its facts are missing or stale, as archspec check --reflect does for
every producer. That boots the application during a check, so leave it off
where checks must not boot anything:
reflect :rails, auto: true
Otherwise, check and explain read the directory’s .yml files without
running the application. A missing directory is an error, and stale facts fail the check as
facts.stale violations. facts alone selects the Rails producer. See
Rails reflection and
Framework integrations.
Components
component :controllers, in: "app/controllers/**/*.rb"
component :models, in: "app/models/**/*.rb"
component :views, in: "app/views/**/*.erb"
component :billing, namespace: "Billing"
component :records, descendants_of: "ApplicationRecord"
component :workflows,
in: "app/models/**/*.rb",
except: "app/models/**/*_workflow.rb"
except: subtracts from the in: patterns and from descendants_of:.
Explicit namespace: and constants: selectors remain explicit. The same hash
form works inside architecture options such as layers: and components:.
Files matched by in: patterns are always analyzed, even when source does not
cover them. ArchSpec ignores .git, .bundle, node_modules, tmp, and
vendor by default, and those defaults step aside for an in: pattern whose
literal leading directories, before the first *, ?, [, or {, sit inside
one of them. in: "vendor/engines/*/**/*.rb" analyzes the engines under
vendor/engines, minus any except: matches. A broad pattern such as
in: "**/*.rb" does not reach into vendor, tmp, or node_modules. Patterns
you pass to ignore always win, including one that restates a default such as
ignore "vendor/**/*".
Declare one component per subdirectory with each_directory, which is handy for engines and packs:
each_directory "packs/*" do |name, path|
component name, in: "#{path}/**/*.rb"
end
Architectures
architecture :layered, layers: {
interface: "app/controllers/**/*.rb",
application: "app/services/**/*.rb",
domain: "app/models/**/*.rb"
}
Architectures define components and rules together. See Architectures.
Dependency Rules
controllers.can_only_use :models, :services
models.cannot_use :controllers
shared_kernel.can_only_be_used_by :billing, :catalog
can_only_use is an allowlist for what a component may depend on. cannot_use forbids specific components. can_only_be_used_by is the inverse of can_only_use: it limits who may depend on the component. See Dependency Rules.
Method Rules
services.must_implement :call
services.must_implement_one_of :call, :resolve
services.cannot_call :render, :redirect_to, :params, :session
jobs.must_implement :perform_later, scope: :class
commands.must_implement :call, arity: 1, keywords: :actor
models.cannot_call :find_by_sql, receiver: "ActiveRecord::Base"
RubyDEX resolves static constant receivers, class-side ancestry, method aliases, and method signatures for these checks. Calls through dynamic receivers remain unknown rather than guessed. See Method Rules and Protocol Rules.
For projects that avoid anonymous command-object style APIs, forbid method definitions too:
library.cannot_define :call
library.cannot_instantiate_and_invoke
See Object Rules.
Constant Rules
models.cannot_reference_constants "ActionController", "ActionView"
Use this when the dependency is better expressed as a framework constant than a component.
See Constant Rules.
Cycles
no_cycles
no_cycles checks component dependency cycles.
See Cycle Rules.
Reasons
Every rule-creating call accepts because::
models.cannot_use :controllers,
because: "models must remain independent of the request"
The reason is printed with each finding and included in JSON, but stays outside the todo fingerprint. Adding or editing a reason does not invalidate an existing todo file. Repeating the same merged rule with two different reasons is rejected when the configuration loads.
Suppressions
# archspec:disable-next-line dependencies.forbid -- legacy export
Admin::UsersController
Supported forms:
archspec:disable-next-line RULE -- reason
archspec:disable-line RULE -- reason
archspec:disable RULE -- reason
archspec:enable RULE
Omit RULE to suppress all ArchSpec rules on that line or block.
In ERB templates, suppressions work in ERB comments, comment-only Ruby tags
(<% # ... %> or <%- # ... -%>), and inline Ruby comments:
<%# archspec:disable-next-line dependencies.forbid -- legacy export %>
<%= Admin::UsersController.name %>
<%= User.count # archspec:disable-line dependencies.forbid %>
<% # archspec:disable dependencies.forbid %>
<%= User.count %>
<% x = 1 # archspec:enable dependencies.forbid %>
<%= User.count %>
The final expression is checked again after enable. Suppressions use physical
template lines, and inline comments end at their ERB closing tag. Escaped tags
such as <%% # archspec:disable %> are literal text and do not suppress checks.