runtimeanalyz.ing ● early access · sign-up open

← All capabilities

what_a_change_touches

CLI · MCP free

What a change touches. Every caller that relies on a method's observed shape, the argument classes it passes and the messages it sends to what comes back, so you know what a signature change reaches before you make it.

Invoice safe
debit ( total Money )
Finance::Accounts changing
def debit(amount, memo:)
  Entry.create!(…)
end
Transfer changes
debit ( amount Integer )
pennies
FeeSchedule changes
debit ( fee Integer )
pennies
Refund changes
debit ( amount Money )
entry Finance::Entry
id
sent to the returned entry
Finance::Entry same object id
Each caller's argument classes come from the calls it made, and the returned entry is followed by its identity to the messages sent to it afterwards. Only the callers whose shape the change breaks are marked.

The problem

Finance::Accounts#debit takes an amount in integer pennies and returns the ledger entry it made. You want it to take Money, and to stop returning the entry, because half the callers ignore it:

module Finance
  class Accounts
    def debit(amount, memo:)
      Entry.create!(account: self, amount: -amount, memo:)
    end
  end
end

Two changes to one method’s shape. Which callers does each one break? Finding the callers is the easy part. Knowing which of them pass an Integer, and which of them use the return value and how, means reading every one of them and following each value by hand. The type checker doesn’t help: there are no declared types to check.

The fix

Ask before you change it:

$ ra what_a_change_touches Finance::Accounts#debit

argument 1 · amount
  Money     Invoice#settle          invoice.rb:41      1,204 calls
  Money     Refund#process          refund.rb:22          57 calls
  Integer   Transfer#call           transfer.rb:9        388 calls   ← changes
  Integer   FeeSchedule#charge      fee_schedule.rb:30    12 calls   ← changes

return value · Finance::Entry
  receives #id       Refund#process   refund.rb:23     ← changes
  unused             Invoice#settle, Transfer#call, FeeSchedule#charge

So the argument change touches two callers, both of which still pass pennies, and the return change touches one, which reads #id off the entry to link the refund to it. Everything else is safe.

Make the change and fix exactly those three lines:

# transfer.rb:9
public_send(direction, Money.from_pennies(amount), memo:)

# fee_schedule.rb:30
account.debit(Money.from_pennies(fee), memo: "fee")

# refund.rb:22, the one caller that needs the entry
entry = ledger.record_debit(amount, memo: "refund #{id}")
update!(entry_id: entry.id)

record_debit keeps the old behaviour under a name that says it hands something back, and debit stops pretending it does.

How it works

Three things in the recording answer it:

  1. Callers. Every recorded call edge into debit, with the method and line it came from.
  2. Argument classes. Each call records the class of every argument it passed, so each caller’s amount is known to be a Money or an Integer from the calls it actually made.
  3. What happens to the return. Each call records the identity of the object it returned. Within the same call tree, any later call whose receiver is that same object is a message sent to the return value. Grouped by caller, that’s how each one uses what debit gives back: Refund sends #id, the others send nothing.

A caller whose argument classes and return usage would still fit the new shape isn’t listed as changing.

Limits

  • Only what ran. A caller that never ran in any recorded run isn’t in the list, and neither is the argument class it would have passed. Pair it with a search for a method this important.
  • Shape comes from usage. A return value that’s passed along untouched, or stored and read later in a different run, has no messages sent to it in the tree that returned it, so it looks unused.
  • App methods only. Messages sent to a returned Integer or String are core methods, and those aren’t recorded. For a return value of one of your own classes, like Entry, they are.

what_calls

CLI · LSP · MCP free

Who can call this method. Every caller of a method from real runs, with where and how often. The calls that actually happened, not grep. what_it_calls does the reverse.

See more →

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.

See more →

what_it_receives

CLI · LSP · MCP free

What a method receives. The observed shape of each argument at a call site. The messages the method actually sends to what it's passed, not only the class it was given.

See more →