runtimeanalyz.ing

← All capabilities

observed_shape

CLI · LSP · MCP Pro

The interface a call depends on. The messages your code sends to a value, read from recorded runs. They are the interface a call depends on, whether or not anyone declared it.

Enrollment caller
def unlock_next
  books = curriculum
    .unlocked_for(student)
  books.select(&:unlocked?)
       .map(&:id)
end
unlocked_for ( student Student )
[book, book, …] Array<Book>
Curriculum receiver
def unlocked_for(student)
  eligible(student)
end
Observed contract Array#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#select or 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.

verify_mock

CLI · LSP · MCP · CI free

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

CLI · LSP · MCP Pro

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

MCP · CLI Pro

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 →