Files
whetrails/handoff/architecture.md
bill ab1f29269e
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
Update handoff docs for completed engine layer
Mark engine as done in overview and build order. Update next-steps to
focus on test gap analysis. Add engine file map, data flow, and dev
setup quirks (Zeitwerk conflict, engine routes conflict) to architecture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:14:19 -06:00

6.8 KiB

Architecture

File map

lib/
  whetrails.rb                  Public entry point. Requires engine if Rails is loaded.
  whetrails/
    version.rb                  VERSION constant (0.1.0)
    chain.rb                    Data structures (ModelChain, CallbackEntry, ValidatorEntry)
    inspector.rb                Core engine — reads Rails internals, builds ModelChain
    formatter.rb                Terminal renderer — formats ModelChain as plain text
    cli.rb                      CLI logic — boots Rails, resolves model name, calls inspector
    engine.rb                   Rails::Engine definition (isolate_namespace Whetrails)
    engine_routes.rb            Engine routes — resources :models + root

app/
  controllers/whetrails/
    application_controller.rb   Base: before_action loads all user-defined AR models
    models_controller.rb        index (redirect to first model), show (render chain)
  helpers/whetrails/
    models_helper.rb            vscode_uri, short_path, condition_label, validator_type_label
  views/
    layouts/whetrails/
      application.html.erb      Layout: sidebar model list + main content area
    whetrails/models/
      index.html.erb            Empty — index action always redirects
      show.html.erb             Lifecycle timeline + validators table

exe/
  whetrails                     Executable entry point — calls Whetrails::CLI.run(ARGV)

whetrails.gemspec               Gem metadata and dependencies

app/models/order.rb             Development fixture model with sample callbacks/validators

Data flow

CLI

exe/whetrails
  → CLI#run_inspect
    → CLI#boot_rails          loads the host app's config/application.rb
    → CLI#resolve_model       turns "Order" string into Order class constant
    → Inspector#call
        reads Model._validation_callbacks  (before/after_validation)
        reads Model._save_callbacks        (before/after_save)
        reads Model._create_callbacks      (before/after_create)
        reads Model._update_callbacks      (before/after_update)
        reads Model._destroy_callbacks     (before/after_destroy)
        reads Model._commit_callbacks      (after_commit)
        reads Model._rollback_callbacks    (after_rollback)
        reads Model.validators             (all validators)
        → returns ModelChain
    → Formatter#render          prints to stdout

Engine (browser UI)

GET /whetrails/
  → ModelsController#index
    → eager_load! + discover user models via Object.const_source_location
    → redirect_to first model

GET /whetrails/models/:id   (:id is the model name, e.g. "Order")
  → ModelsController#show
    → params[:id].constantize → model class
    → Inspector#inspect_model → ModelChain
    → renders show.html.erb: lifecycle timeline grouped by event,
      each callback shows filter name, vscode:// source link, condition badges,
      module badge if defined in a concern

Key Rails API details

These are the actual Rails internals the inspector uses — documented here because they are undocumented in official Rails docs and took investigation to discover.

Callbacks

Each _<event>_callbacks method returns an ActiveSupport::Callbacks::CallbackChain. Each entry in the chain is an ActiveSupport::Callbacks::Callback with:

Field Access Notes
Lifecycle stage cb.kind :before, :after, or :around
Method name cb.filter Symbol (method name) or Proc
if: conditions cb.instance_variable_get(:@if) Array — .options does NOT exist
unless: conditions cb.instance_variable_get(:@unless) Array

cb.options does not exist in Rails 8 — this is a common mistake from outdated docs. Conditions are stored in @if / @unless instance variables.

Source location

# For symbol callbacks:
model.instance_method(cb.filter).source_location  # => ["path/to/file.rb", 42]
model.instance_method(cb.filter).owner            # => the module that defines it

# For proc callbacks:
cb.filter.source_location

Rails internal filter

Two types of internals are filtered out:

  1. Callbacks whose source file is inside a Rails gem (/gems/activerecord-* or /gems/activemodel-*)
  2. Conditions that are ActiveSupport::Callbacks::Conditionals::* instances (Rails adds these internally to after_create / after_update to guard against wrong lifecycle stage)

Validators

Model.validators           # => Array of validator objects
validator.class            # => ActiveRecord::Validations::PresenceValidator, etc.
validator.attributes       # => [:status]
validator.options[:if]     # => conditions (NOT instance variables — different from callbacks)
validator.options[:unless]

Dev setup quirks

This repo is both the gem source and the development Rails app. That creates two issues that were solved and should not be undone:

1. Zeitwerk autoloading conflict

config/application.rb has config.autoload_lib(ignore: %w[assets tasks whetrails.rb whetrails]). The whetrails.rb and whetrails entries are critical: without them, Zeitwerk tries to autoload lib/whetrails/chain.rb as Whetrails::Chain (wrong constant name), crashing on every request. The gem files use manual require_relative — they must not be Zeitwerk-autoloaded.

2. Engine routes conflict

lib/whetrails/engine.rb overrides the engine's routing path:

config.paths.add "config/routes.rb", with: "lib/whetrails/engine_routes.rb"

Without this, Rails uses config/routes.rb as the engine's routes file (same directory), which causes mount Whetrails::Engine to execute twice and fail with a duplicate route name error. Engine routes live in lib/whetrails/engine_routes.rb instead.

3. Gem not in Gemfile by default

The gem is listed as gem "whetrails", path: "." in Gemfile. This is intentional — Bundler.require must explicitly load the gem so Whetrails::Engine is defined before config/routes.rb tries to mount it.

Known limitations

  • Proc source location: Proc-based callbacks (e.g. before_save { do_something }) report source location from Ruby's Proc#source_location, which may point inside a concern or DSL wrapper rather than the user's code. This is a Ruby limitation.

  • _callbacks vs __callbacks: The research phase incorrectly identified _callbacks as the API. The actual method is __callbacks (two underscores) for the hash, or individual chain accessors like _save_callbacks. The individual accessors are used in this codebase.

  • Validator source location: Not yet implemented. Validators don't expose source location directly. A future approach would be to patch validate / validates at load time to capture it.

  • around callbacks: Registered but the lifecycle semantics are more complex (they wrap the operation with yield). Not currently rendered differently from before/after.