runtimeanalyz.ing ● early access · sign-up open

← All capabilities

observed_shape

CLI · LSP · MCP free

The observed contract. The messages your code actually sends to a value, read from real runs. 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 · 12 examples · 3,181 prod runs
Enrollment passes a Student and gets back an Array of books. The messages it then sends to that array and each book in it are the contract, whatever the signature says.

The problem

Ask what Curriculum#unlocked_for returns and you’ll get one of three answers. The code says “whatever eligible returns”, a type signature (if there is one) says Array, and neither says what the caller needs. Calling code relies on something much more specific: a collection whose elements answer #unlocked? and #id.

That specific thing is the contract that matters. It’s what a double has to honour, what a replacement class has to implement, and what a refactor must not break. It’s written down nowhere.

The fix

Your agent asks over MCP before writing a double:

observed_shape("Curriculum#unlocked_for", as: "return")
→ {
  return:   { class: Array,
              responds: [select, each, any?] },
  element:  { class: Book,
              responds: [unlocked?, id, title] },
  examples: 12,
  prod_runs: 3181
}

In your editor it’s an inlay hint on the call:

def unlock_next = curriculum.unlocked_for(student)
# ⤷ Array<Book> · #unlocked? #id #title

On the command line:

$ ra observed_shape Curriculum#unlocked_for

return   → #select #each #any?        Array
element  → #unlocked? #id #title      Book
· from 12 spec examples and 3,181 production runs

Now the double can honour the contract the caller actually relies on:

book = instance_double(Book, unlocked?: true, id: 1, title: "Geometry")
curriculum = instance_double(Curriculum, unlocked_for: [book])

Ask for an argument instead (as: "argument") and you get the messages the method sends to what it was passed: the interface a caller has to supply.

How it works

The recording keeps the identity of every object a traced method returns. observed_shape follows that object forward to every later call where it’s the receiver, and collects the messages sent to it. For a collection, it does the same for the elements that come out of it.

The result has two halves. The messages are the shape your code depends on. The classes are the ones that actually turned up, so you can see when three unrelated classes all satisfy the same shape.

Limits

  • Only what ran. A value’s shape is the messages it received in recorded runs. Code paths no spec or run reached contribute nothing.
  • Values passed straight through are invisible. If a method returns something that’s handed on untouched and never called, there’s no shape to observe.
  • Most reliable close to the call. Following an object within a method is exact. Once it’s been stored and passed around, the messages sent to it may come from code with other expectations.

verify_mock

CLI · LSP · MCP · CI free

Catch a lying mock. A stub that passes green but returns something the real collaborator never would. Mutation testing can't see it. The recording can.

See more →

split_interface

CLI · LSP · MCP free

Split a fat interface. A class whose callers each use a different slice of it. It names the role interfaces to split it into, measured from who actually calls what.

See more →

how_to_reach

MCP free

Reach any method. The collaborators and call path that get an object into the state a method needs, taken from a real run. Setup for tests that use real objects instead of doubles.

See more →