def unlock_next
books = curriculum
.unlocked_for(student)
books.select(&:unlocked?)
.map(&:id)
end def unlocked_for(student)
eligible(student)
end #select · each Book#unlocked?#id · 9 spec examples · 42 app runs Enrollment passes a Student and gets back an Array of books.
The messages it then sends to that array and to each book in it are the contract the call depends
on.
The problem
There are two places to look up what Curriculum#unlocked_for returns. The code says “whatever
eligible returns”, and a type signature, if there is one, says Array. The calling code relies on
something more specific than either: a collection whose elements answer #unlocked? and #title.
A double, a replacement class and a refactored method all have to satisfy that contract, and it is written down nowhere in the source.
The fix
On the command line:
$ ra observed_shape Curriculum#unlocked_for
Curriculum#unlocked_for
· observed return
return → (no app messages) Array
element → #unlocked? #title Book
· from 9 spec examples and 42 app runs
The Array itself receives no app messages, because select and map are core methods and the
recorder only sees methods your app defines. The elements are Books, and the app sends them
#unlocked? and #title.
Your agent asks the same over MCP and gets the text plus the blocks behind it:
{
"title": "Curriculum#unlocked_for",
"summary": "observed return",
"blocks": [
{ "type": "table", "columns": ["kind", "receives", "class"],
"rows": [["return", "→ (no app messages)", "Array"],
["element", "→ #unlocked? #title", "Book"]] },
{ "type": "text", "lines": ["· from 9 spec examples and 42 app runs"] }
]
}
In your editor, hovering over the def shows the same shapes and the shape of the argument:
Curriculum#unlocked_for · 3 callers
returns Array (628)
elements Book (1506) → #unlocked? #title
arg 1: Student (628) → #completed?
With that shape you can write a double that honours the contract the caller relies on:
book = instance_double(Book, unlocked?: true, title: "Geometry")
allow(curriculum).to receive(:unlocked_for).and_return([book])
With --as argument --position 1 it reports the messages the method sends to what it was passed,
which is the interface a caller has to supply. For Report#summary_line that is #title #weeks,
from 65 real Books and one instance_double.
How it works
The recording keeps the identity of every object a traced method returns, and for an Array the
identities of its elements. observed_shape follows each object forward to every later call in the
same call tree where it’s the receiver, and collects the messages sent to it.
The result pairs the messages, which are the shape your code depends on, with the classes that were observed receiving them, so you can see when three unrelated classes all satisfy the same shape.
Run it on your app
The output on this page comes from our example Rails app, and it is regenerated whenever a tool
changes. These are the commands behind it. On your own app, run ra record first, then put your
own class, method or file in their place.
ra observed_shape 'Curriculum#unlocked_for'
ra observed_shape 'Report#summary_line' --as argument --position 1
The method is app/models/curriculum.rb:15. The consumers that send #unlocked? and #title are
app/controllers/progress_controller.rb:4 and app/models/enrollment.rb:18.
Reading the result
- Messages handled by core or gem code, such as
Array#selector an ActiveRecord attribute reader, are not recorded, so they do not appear in a shape. A method that a contract depends on does appear once it is defined in one of your own classes. - A value that is passed straight through has no shape. If a method returns something that is handed on untouched and never called, there is nothing to observe.
Every tool reads the same recording. What gets recorded and how far to trust an answer are on the questions page.
Related tools
verify_mock
Stubs that disagree with the real object. Compares what each stub in a spec returns with what the real method returned in the recording, and reports the stubs that return a class the real method did not.
See more →split_interface
The role interfaces in a class. Groups a class's public methods by which callers used them in the recording, and lists each group as a role interface to extract.
See more →how_to_reach
The setup a method needs. The collaborators and the call path that put an object into the state a method needs, taken from a recorded run. Use it to write setup for tests that use real objects in place of doubles.
See more →