Add handoff documentation
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 <noreply@anthropic.com>
This commit is contained in:
105
handoff/architecture.md
Normal file
105
handoff/architecture.md
Normal file
@@ -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 `_<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
|
||||||
|
|
||||||
|
```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.
|
||||||
72
handoff/next-steps.md
Normal file
72
handoff/next-steps.md
Normal file
@@ -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`
|
||||||
32
handoff/overview.md
Normal file
32
handoff/overview.md
Normal file
@@ -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 <ModelName>` 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
|
||||||
Reference in New Issue
Block a user