Rails Ecosystem Plugins — Roadmap
Status: planning, 2026-05-08. This document captures the
planned rigor-* plugin family for Rails apps. It is informational;
the binding sources for individual plugin contracts remain the
README.md and integration spec under each plugin’s directory.
Superseded premise (2026-06-02): the subtree-split distribution model is retired. This roadmap was written assuming each plugin would eventually be
git subtree splitinto its own separately- published gem. That model was dropped — bundled plugins now ship inside the singlerigortypegem (per-plugin gemspecs removed in commit9769f5fa), and ADR-31 made third-party plugins live in their authors’ own repos (WD4) with subtree merge kept only as a rare reserved import option (WD5), never the planned outbound flow. Read the “Working principles” subtree-split language, the “Subtree-split readiness checklist”, and the per-plugin “extract” steps below as historical context, not current plan. The tier table, dependency graph, and per-plugin behaviour sketches remain accurate and useful; only the distribution premise changed.
The first plugin in this family — rigor-activerecord —
landed on master (commit e8fda84) and is staged in the
monorepo per skills/rigor-plugin-author/SKILL.md’s
“start in monorepo” discipline. (The original “extract via
git subtree split once stable” half of that discipline is retired —
see the superseded-premise note above.)
Working principles
Section titled “Working principles”- Each plugin will be subtree-split to its own repository
(
rigortype/rigor-<id>) once its contract has stabilised against a real Rails consumer. The monorepo is the incubator; the eventual home is independent gems. - Per-plugin
demo/directories ship with their plugin. No shared Rails-app skeleton across plugins — after subtree-split eachdemo/travels with its plugin and must be self-contained. Some duplication of Rails-shaped directory tree (e.g.app/models/application_record.rb) is accepted in exchange for clean extraction. - Real-Rails alignment is a goal, not a runtime
dependency. The plugin source code does NOT
require "rails"/require "active_record". It analyses project source files. But the plugin’s behaviour (path helpers generated, column types accepted, filter chains recognised) MUST match what real Rails generates / accepts for the same input. Integration specs that compare plugin output against a small real Rails app’srails routes -E/ schema dump output are encouraged. - Cross-plugin facts go through a shared API. The
rigor-actionpackstrong-params consumer needs the model indexrigor-activerecordbuilds. That cross-plugin handoff is via the v0.1.x cross-plugin API (ADR-9), not via duplicated reads or shared cache producer ids.
Plugin tier table
Section titled “Plugin tier table”Tier 1 plugins land first because they have the highest user value AND do not require new analyser-side API. Tier 2 either extends an existing plugin or needs the cross-plugin API ADR-9 ships. Tier 3 is specialised — author when there is concrete user demand.
| Tier | Plugin | Scope | API needs |
|---|---|---|---|
| 1A | rigor-rails-routes | Real config/routes.rb DSL → *_path / *_url validation | Current API |
| 1B | rigor-rails-i18n | config/locales/*.yml → t('key.path') validation | Current API |
| 1C | rigor-actionmailer | Mailer methods + view template existence | Current API |
| 1D | rigor-activejob | Job perform arity | Current API |
| 2A | rigor-activerecord extension | associations, enums, scopes, validations, callbacks | Current API; landed as 0.2.0+ of the existing gem |
| 2B | rigor-actionpack Phase 1 | Strong parameters → AR column validation | Cross-plugin API (ADR-9) |
| 2C | rigor-factorybot | Factory attribute → AR column validation | Cross-plugin API |
| 2D | rigor-actionpack Phase 2-4 | Filter chains, render targets, route-helper consumption | Cross-plugin API |
| 3A | rigor-rspec | let / subject / mock target validation | Current API |
| 3B | rigor-pundit | Policy method existence + authorize arg validation | Current API |
| 3C | rigor-sidekiq | Worker perform arity, queue config | Current API |
| 3D | rigor-graphql | Schema → resolver argument types | Current API |
| 3E | rigor-activestorage | has_one_attached macros + generated methods | Cross-plugin API |
| 3F | rigor-actioncable | Channel methods + broadcast names | Current API |
After Tier 1+2 lands, rigor-rails becomes a meta-gem that
declares these dependencies in its gemspec and lets users add
one line to their Gemfile to opt into the whole stack.
Plugin sketches
Section titled “Plugin sketches”rigor-rails-routes
Section titled “rigor-rails-routes”Tier 1A — current API. Parses real config/routes.rb (not
the YAML simplification examples/rigor-routes/ uses for
teaching purposes).
DSL surface for v0.1.0 of the plugin:
Rails.application.routes.draw do ... endblockresources :name [, only: [...] | except: [...]]resource :nameget/post/patch/put/delete "/path", to: "controller#action", as: :nameroot to: "controller#action"- Nested
resources(1 level deep) member do ... end/collection do ... endnamespace :admin do ... end(prefixes path + helper name)
Out of scope for v0.1.0:
scope :module:/scope :path:/scope :as:- Constraints (
constraints: { id: /\d+/ }) - Custom
direct(:name) { |obj| ... } - Mountable engines (
mount Sidekiq::Web => "/sidekiq") - Format restrictions
Diagnostics:
controllers/users_controller.rb:42:7: info: `user_post_path` → GET /users/:user_id/posts/:idcontrollers/users_controller.rb:50:1: error: no route helper `widgts_path` (did you mean `widgets_path`?)controllers/users_controller.rb:51:1: error: `user_path` expects 1 argument (:id), got 0Architecture: Mirrors rigor-activerecord’s SchemaParser
recursive-descent on Prism, plus rigor-routes’ helper-name
table. Helper generation rules need careful real-Rails
verification — see “Real-Rails alignment” below.
Real-Rails alignment: integration spec compares the
plugin’s HelperTable against rails routes -E’s output for
the same config/routes.rb. A small Rails app under
demo/ provides the reference.
rigor-rails-i18n
Section titled “rigor-rails-i18n”Tier 1B — current API. Validates t('key.path') against
config/locales/*.yml.
Surface:
t('key.path')/I18n.t('key.path')/I18n.translate('key.path')t('key.path', interpolation_var: value)— validates the interpolation keys against the%{var}placeholders in the locale valuel(time, format: :short)— validates:shortagainst the locale’s date format keys
Out of scope for v0.1.0:
- Lazy lookup (
t('.title')resolved against the rendered controller / view path — needsrigor-actionpack) - Locale fallbacks chains
- Plural rules
Diagnostics:
view.html.erb:5:1: info: `t('users.welcome')` resolves in en, jaview.html.erb:8:1: error: missing key `users.welcom` in en (did you mean `users.welcome`?)view.html.erb:12:1: error: `users.welcome` expects interpolation `name`, got `username`Architecture: rigor-routes (YAML reads) + rigor-pattern
(literal-string gating for t(literal_key)). Glob-loop the
locale paths through IoBoundary.
rigor-actionmailer
Section titled “rigor-actionmailer”Tier 1C — current API. Validates Mailer call shape and view path existence.
Surface:
class UserMailer < ApplicationMailer def welcome(user) @user = user mail(to: user.email) endend
UserMailer.welcom(user).deliver_now # error: undefined methodUserMailer.welcome.deliver_now # error: missing required argUserMailer.welcome(user, foo: 1) # error: wrong arityPlus existence check for app/views/<mailer_underscore>/<method_name>.{html,text}.erb.
Architecture: rigor-activerecord’s ModelDiscoverer
pattern adapted to mailer classes (subclass of
ApplicationMailer / ActionMailer::Base). View path checked
via IoBoundary.
rigor-activejob
Section titled “rigor-activejob”Tier 1D — current API. Validates Job.perform_later
argument arity against the job class’s #perform definition.
Surface:
class WelcomeEmailJob < ApplicationJob def perform(user_id, locale = "en") ... endend
WelcomeEmailJob.perform_later(123) # infoWelcomeEmailJob.perform_later # error: missing user_idWelcomeEmailJob.perform_later(123, "ja", :foo) # error: wrong arityArchitecture: Tiny — class discovery + per-call arity check.
Same pattern as rigor-actionmailer.
rigor-actionpack
Section titled “rigor-actionpack”Tier 2B+2D — needs cross-plugin API (ADR-9). The flagship
“Rails apps want this” plugin, but its primary value comes
from cross-checking against rigor-activerecord’s model
index. Phased rollout:
Phase 1 — strong parameters
Section titled “Phase 1 — strong parameters”def user_params params.require(:user).permit(:name, :emial) # error: column `:emial` not on table `users` (did you mean `:email`?)endReads ADR-9’s services.fact_store for rigor-activerecord’s
:model_index fact. Resolves :user (Symbol arg of require)
to the User model, then validates permit keys against the
table.
Phase 2 — filter chains
Section titled “Phase 2 — filter chains”class UsersController < ApplicationController before_action :authenticate, only: [:create, :update] # validates :authenticate exists as an instance method # validates :create, :update exist as actionsendTwo-pass within the controller class: collect action method
declarations, then validate filter :method_name and only: /
except: Symbol lists.
Phase 3 — render targets
Section titled “Phase 3 — render targets”def show render partial: "users/profile", locals: { user: @user } # validates app/views/users/_profile.html.erb existsendIoBoundary checks for the partial file’s existence.
Phase 4 — route-helper consumption
Section titled “Phase 4 — route-helper consumption”def show redirect_to user_path(@user)endConsumes rigor-rails-routes’ :helper_table fact through
ADR-9. Validates the helper name + arity at the call site (not
at the controller-defining file — the controller might be
called from anywhere).
rigor-factorybot
Section titled “rigor-factorybot”Tier 2C — needs cross-plugin API. Factory attribute validation against AR columns.
FactoryBot.define do factory :user do name { "Alice" } invlid_attribute { "x" } # error if rigor-activerecord is loaded endend
create(:usre) # error: factory undefined (did you mean :user?)build(:user, emial: "x") # error: column mismatchTwo-phase: discover factory definitions (similar to
rigor-statesman), then validate use sites. Consumes
rigor-activerecord’s model index via ADR-9 fact_store.
rigor-rspec
Section titled “rigor-rspec”Tier 3A — current API. Test DSL flow tracking.
RSpec.describe User do let(:user) { User.new(name: "Alice") } subject(:greeting) { "Hello, #{user.name}" }
it "greets" do expect(greeting).to eq("Hello, Alice") expect(user).to receive(:nme).and_return("X") # error: no method :nme on User endendHeavy implementation (RSpec DSL is broad). Expected size: 600+ lines. Author when test-side validation becomes a clear priority — likely Tier 3 because Rails apps benefit more from controller / model / view validation first.
rigor-pundit
Section titled “rigor-pundit”Tier 3B — current API. Policy method existence + authorize
arg validation.
authorize @user, :update?authorize @user, :destory? # error: undefined policy method (did you mean :destroy?)Policy class discoverer + per-call validation. Conventional
mapping: User → UserPolicy, action method :update? →
UserPolicy#update?. cancancan is a separate plugin with
similar shape but different convention.
Plugin dependency graph
Section titled “Plugin dependency graph” ┌────────────────────────┐ │ rigor-activerecord │ │ (already landed) │ └──┬─────────────────────┘ │ publishes :model_index via fact_store ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ │ rigor-actionpack │ │ rigor-factorybot │ │ rigor-active- │ │ Phase 1 (params) │ │ │ │ storage │ └────────────────────┘ └──────────────────────┘ └─────────────────┘ ▲ │ consumes :helper_table │ ┌──────────┴───────────┐ │ rigor-rails-routes │ │ publishes :helper_ │ │ table │ └──────────────────────┘
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │ rigor-rails-i18n │ │ rigor-actionmailer │ │ rigor-activejob │ │ (independent) │ │ (independent) │ │ (independent) │ └──────────────────────┘ └──────────────────────┘ └──────────────────┘
┌──────────────────────┐ ┌──────────────────────┐ │ rigor-rspec │ │ rigor-pundit │ │ (independent) │ │ (independent) │ └──────────────────────┘ └──────────────────────┘rigor-rails (meta-gem) sits above all of these and pulls
them in via gem dependencies. Users who want the whole stack:
gem "rigor-rails".
Demo / test app strategy
Section titled “Demo / test app strategy”Per-plugin self-contained demos. Each plugins/rigor-<id>/demo/
ships a small Rails-shaped directory tree appropriate to the
plugin’s scope. After git subtree split, the demo travels
with the plugin without manual fix-up.
For Tier 1+2 plugins that need a cross-cutting Rails app
(strong params + AR + routes), the demo is still per-plugin —
each plugin’s demo includes only the Rails surfaces THAT
plugin needs. rigor-actionpack’s demo carries a controller
file and an application_record.rb for the model fixture, but
NOT a full Rails directory tree.
For real-Rails verification: integration specs may exec a
small rails new skeleton in a tmpdir and compare plugin
output against rails routes -E / db:schema:dump etc., but
that is a TEST-time tool, not a demo-time fixture.
Subtree-split readiness checklist
Section titled “Subtree-split readiness checklist”Per plugin, verify before splitting:
-
plugins/rigor-<id>/directory is self-contained (norequire_relatives pointing outside). -
plugins/rigor-<id>/demo/runs cleanly viaRUBYLIB=$PWD/../lib bundle exec rigor check. - Integration spec at
spec/integration/examples/<id>_plugin_spec.rbpasses with the plugin loaded as a realPlugin::Loader.loadconsumer. - Plugin’s
gemspecdeclares the right semver range onrigortype(e.g.>= 0.1.0, < 0.2.0). - No cross-plugin file references — cross-plugin
data flows only through
services.fact_store(post-ADR-9) or through duplicated reads (pre-ADR-9). - README has the “Future direction” section explaining what’s queued post-extraction.
When all check, run:
git subtree split --prefix=plugins/rigor-<id> -b rigor-<id>-extractedgit remote add rigor-<id> git@github.com:rigortype/rigor-<id>.gitgit push rigor-<id> rigor-<id>-extracted:masterThen in the monorepo: remove plugins/rigor-<id>/, drop the
matching spec/integration/examples/<id>_plugin_spec.rb,
update examples/README.md’s comparison table to remove the
row, and update README.md’s plugin list.
Order of operations
Section titled “Order of operations”- Document the plan — this file + ADR-9. (Current commit.)
- Implement Tier 1 plugins (current API) —
rigor-rails-routes,rigor-rails-i18n,rigor-actionmailer,rigor-activejob. Each as its own commit, with subtree-split readiness in mind. - Implement cross-plugin API (ADR-9) —
Plugin::FactStore+prepare(services)hook +consumes:manifest field + topological sort inPlugin::Loader. Update public-API drift snapshots. Add the SKILL section. - Implement Tier 2 plugins (cross-plugin) —
rigor-actionpackPhase 1 (strong params), thenrigor-factorybot. These exercise ADR-9 against real consumers. - Stabilise + extract — once each plugin’s
examples/directory has been stable for ≥ 2 releases, run the subtree-split flow and migrate to a separate repo. - Tier 3 + meta-gem — author Tier 3 plugins as user demand
surfaces. Once Tier 1+2 are extracted, publish
rigor-railsmeta-gem with the dependency aggregation.
The Tier 1 plugins (current API) are blockers only on authoring time, not on contract design. They can land in parallel by independent implementers if desired. Tier 2 blocks on ADR-9 implementation.
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.