From 6dd78f2487a20ebb9edb79be10f42888418ebbdc Mon Sep 17 00:00:00 2001 From: bill Date: Mon, 8 Jun 2026 10:27:39 -0600 Subject: [PATCH] Add handoff documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overview, architecture notes, and next steps for anyone picking this up — including Rails API gotchas discovered during implementation. Co-Authored-By: Claude Sonnet 4.6 --- handoff/architecture.md | 105 ++++++++++++++++++++++++++++++++++++++++ handoff/next-steps.md | 72 +++++++++++++++++++++++++++ handoff/overview.md | 32 ++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 handoff/architecture.md create mode 100644 handoff/next-steps.md create mode 100644 handoff/overview.md diff --git a/handoff/architecture.md b/handoff/architecture.md new file mode 100644 index 0000000..57626fe --- /dev/null +++ b/handoff/architecture.md @@ -0,0 +1,105 @@ +# Architecture + +## File map + +``` +lib/ + whetrails.rb Public entry point. Exposes Whetrails.inspect_model(ModelClass) + 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 + +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 + +``` +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 +``` + +## 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 `__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 + +```ruby +# 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 + +```ruby +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] +``` + +## 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. diff --git a/handoff/next-steps.md b/handoff/next-steps.md new file mode 100644 index 0000000..6a3a93c --- /dev/null +++ b/handoff/next-steps.md @@ -0,0 +1,72 @@ +# Next Steps + +## Immediate: Rails Engine (browser UI) + +Mount a UI at `/whetrails` inside the host Rails app. A developer adds the gem to their +Gemfile, mounts the engine in `config/routes.rb`, starts the server, and visits `/whetrails`. + +### What the UI needs to show + +- A list of all models in the app (left sidebar or index page) +- Click a model → full chain view (same data as CLI, but visual) +- Lifecycle timeline: each stage as a row, callbacks as nodes within it +- Source file/line as a clickable link (opens in editor via `vscode://` URI scheme) +- Conditions rendered inline (`if: :paid?` shown as a badge) +- Callbacks from concerns/gems visually distinguished from model-defined ones + +### Technical approach for the engine + +```ruby +# lib/whetrails/engine.rb +module Whetrails + class Engine < ::Rails::Engine + isolate_namespace Whetrails + end +end +``` + +Rails engine conventions: +- Routes go in `config/routes.rb` inside the gem +- Controllers in `app/controllers/whetrails/` +- Views in `app/views/whetrails/` +- The host app mounts it: `mount Whetrails::Engine, at: "/whetrails"` + +The engine controllers call `Whetrails::Inspector` directly — no duplication of logic. + +### Model discovery + +The engine index page needs to list all AR models in the host app: + +```ruby +Rails.application.eager_load! +models = ApplicationRecord.descendants # or ActiveRecord::Base.descendants +``` + +## After engine: Test Gap Analysis + +Once the chain is extracted, gap analysis is: + +1. Run the test suite with SimpleCov (or equivalent) to get line coverage data +2. For each callback method in the chain, check whether its source lines are covered +3. For each validator, check whether the validation path is exercised +4. Report: "these 3 callbacks have no test coverage" + +The chain data structure (`ModelChain`) is already the right shape for this — each +`CallbackEntry` has `source_file` and `source_line`, which can be cross-referenced +against SimpleCov's coverage report. + +## Product / distribution + +- Publish to RubyGems.org once the engine is working +- The gem should be dev/test-only (add to Gemfile under `group :development`) +- Pricing model TBD — could be open source with a paid cloud version, or MIT with + a commercial license for teams + +## Environment notes + +- Ruby 3.3.6 installed via rbenv (`~/.rbenv/versions/3.3.6`) +- rbenv init added to `~/.zshrc` +- Rails 8.1.3 +- Dev database: SQLite at `storage/development.sqlite3` +- Gitea running in Docker on localhost:3000 (container name: `gitea`) +- Gitea API token for `bill`: `c2948775b80bcaa0ba65c6359658d0d48c340c4e` diff --git a/handoff/overview.md b/handoff/overview.md new file mode 100644 index 0000000..e7fcce0 --- /dev/null +++ b/handoff/overview.md @@ -0,0 +1,32 @@ +# Whetrails — Project Handoff + +## What this is + +A Ruby gem that makes ActiveRecord callback and validation chains visible to developers. + +When a Rails developer calls `.save` on a model, Rails fires a chain of callbacks and validations in a specific order. These are registered in scattered places (the model file, concerns, gems) and Rails never surfaces the full picture in one place. Whetrails extracts and displays that chain: what fires, in what order, where it's defined, and under what conditions. + +This is the first half of a two-part tool. The second half (not yet built) uses the extracted chain as input to a test coverage gap analyzer — showing which callbacks and validators are never exercised by the test suite. + +## Current state (as of 2026-06-08) + +Two layers are complete: + +1. **Core inspector** — extracts the full chain from any ActiveRecord model class at runtime +2. **CLI** — `whetrails inspect ` boots the host Rails app and renders the chain to the terminal + +The next layer (Rails engine / browser UI) has not been started. + +## Build order + +``` +[DONE] 1. Gem core lib/whetrails/inspector.rb +[DONE] 2. CLI exe/whetrails + lib/whetrails/cli.rb +[NEXT] 3. Rails engine browser UI mounted at /whetrails + 4. Test gap layer uses chain data to find uncovered callbacks/validators +``` + +## Repository + +- Gitea: http://localhost:3000/bill/whetrails +- Branch: main