How It Works
After reading this guide, you will know:
- The five stages between your source code and a diagnostic.
- What facts ArchSpec extracts and what it refuses to guess.
- Why checks are fast and safe to run anywhere.
The Pipeline
ArchSpec never loads or executes your application. It reads source code with Prism, Ruby’s own parser, and everything downstream is plain data:
glob files -> parse -> extract facts -> assign components -> evaluate rules
- Collect. The patterns from
source(defaulting toapp/**/*.rb,lib/**/*.rb, and pack/engine paths) are globbed from the project directory, minusignorepatterns. (Analyzer.ruby_files) - Parse. Each file goes through
Prism.parse_file. Syntax errors becomeparser.syntaxdiagnostics instead of crashes, andarchspec:disablecomments are collected as suppressions. - Extract facts. A visitor walks the syntax tree and records edges in
a graph — who defines what, who touches what. (
Analyzer::SourceVisitor) - Assign components. Each
componentdeclaration claims files by glob and constants by namespace. A file can belong to several components; theexplaincommand shows why. (Graph.assign_components) - Evaluate. Every rule reads the graph and emits diagnostics, which are
then filtered through suppressions and the baseline, sorted, and printed.
(
Evaluator.evaluate)
The Facts
Everything a rule can check is one of these edge types:
| Fact | Recorded when |
|---|---|
references_constant |
UsersController appears in an expression |
inherits_from |
class User < ApplicationRecord |
includes / prepends / extends |
include Billable and friends |
calls_named_method |
any method call, by name |
instantiates_and_invokes |
UserBuilder.new(params).build |
requires / requires_relative |
require "csv" with a literal string |
dynamic_feature |
send, const_get, define_method, method_missing, … |
Alongside edges, the graph keeps each constant’s methods and mixins (for
protocol rules) and each file’s expected constant from its path (for the
Zeitwerk rule: app/models/user.rb should define User).
Run archspec explain app/models/user.rb to see the facts for one file —
it prints the expected constant, component assignments with reasons, and
every outgoing edge.
A Violation, Traced
Given models.cannot_use :controllers and this file:
class User
def profile_path = UsersController
end
Parsing records a references_constant edge from app/models/user.rb to
UsersController. Component assignment puts the file in models and puts
UsersController (defined in app/controllers) in controllers. The
dependencies.forbid rule walks dependency edges from models, resolves
UsersController the way Ruby would — innermost namespace outward — finds
it lands in a forbidden component, and emits:
[dependencies.forbid] app/models/user.rb:2:22
models must not depend on controllers
evidence: User references_constant UsersController
confidence: high
Dynamic Code
Static analysis cannot see through send, const_get, or method_missing.
ArchSpec records these as dynamic_feature facts with confidence
unknown_due_to_dynamic_feature instead of ignoring them, and every
diagnostic carries the evidence it was derived from, so you can verify a
report against the source line it points to.
Because ArchSpec only parses code and never loads it, checks need no Rails boot, no database, and have no side effects. They are safe to run in CI, in a git hook, or after an AI-assisted change.