runtimeanalyz.ing ● early access · sign-up open

← All capabilities

stabilize_dep

CLI · MCP free

Point dependencies at stability. A stable module, with many callers and few dependencies, reaching into a less stable one. Measured from real call traffic, so you can depend toward stability or put an abstraction between them.

Against stability

Finance::Accounts instability 0.12
Called from 14 namespaces. Calls into 2.
to_ledger_s
8,930 calls from 1 call site
Formatters::Currency instability 0.81
Called from 2 namespaces. Calls into 9.

Toward stability

Formatters::Currency instability 0.81
to_ledger_s
the churn depends on the core
Finance::Money instability 0.12
Stability is counted from real callers: many callers and few dependencies makes a namespace stable. The ledger was reaching into a report formatter. Moving one method down turns the arrow around.

The problem

Some code is load-bearing. Finance::Accounts is called from fourteen namespaces, and it depends on almost nothing. Everything leans on it, so it has to change slowly and carefully. Other code is meant to churn: Reporting is called by two things and depends on nine, and it changes every time someone wants a new column.

The stable dependencies principle says dependencies should point from the second kind to the first. Here, one points the wrong way:

module Finance
  class Accounts
    def debit(account, amount)
      entry = ledger.append(account, -amount)
      audit.log(Reporting::Formatters::Currency.new(amount).to_ledger_s)
      entry
    end
  end
end

Now a change to a report formatter can break every debit in the app. Nobody decided that. Someone needed a currency string and the formatter was right there. You can’t see it from the file either: the line looks harmless, and how stable each side is only shows up when you count who calls whom.

The fix

Ask which dependencies run against stability:

$ ra stabilize_dep finance

finance → reporting                         against stability
  finance     instability 0.12   called from 14 namespaces, calls into 2
  reporting   instability 0.81   called from 2 namespaces, calls into 9
  1 call site, 8,930 calls:
    Finance::Accounts#debit → Reporting::Formatters::Currency#to_ledger_s
  move it into finance, or depend on an abstraction finance owns      SDP · DIP

One call site is the whole dependency, so the cheap fix is to move the method down to where it’s needed and let reporting depend on finance instead:

module Finance
  class Money
    def to_ledger_s = format("%.2f %s", amount, currency)
  end

  class Accounts
    def debit(account, amount)
      entry = ledger.append(account, -amount)
      audit.log(amount.to_ledger_s)
      entry
    end
  end
end

Reporting::Formatters::Currency now calls Finance::Money#to_ledger_s if it wants the same string. The arrow points toward the stable side. When the dependency is too big to move, the other option is dependency inversion: Finance defines the role it needs, and Reporting supplies an object that plays it.

How it works

  1. Callers in, callees out. From every recorded call edge, rolled up to namespaces, it counts the distinct namespaces that call into each one and the distinct namespaces it calls out to.
  2. Instability. Calls out divided by calls in plus calls out gives a number from 0 (everyone depends on it, it depends on nothing) to 1 (nothing depends on it, it depends on everything).
  3. Flag the wrong direction. Any edge from a clearly more stable namespace to a clearly less stable one is reported, with its traffic and the call sites that make it. Because the edges come from real calls, a dependency made through send or a callback counts the same as a direct one.

Limits

  • Stability from what ran. A namespace your recorded runs barely touched has too few callers to measure fairly. The report says how many calls each number rests on.
  • No abstractness score. The classic metric also weighs how abstract a package is. Ruby has no interfaces to count, so this stops at instability and leaves the abstraction to you.
  • Sometimes it’s fine. A stable core calling out to a plugin point is on purpose. It’s a hint, not a build failure.

break_cycle

CLI · MCP free

Cut a dependency cycle. Files or namespaces caught in a runtime dependency cycle, flagged with the single lowest-traffic edge to sever to break the loop.

See more →

split_package

CLI · MCP free

Split a package used in parts. Consumers that pull in a whole namespace but touch disjoint pieces of it. Split it so nobody takes a dependency on code they never call.

See more →

coupling_map

CLI · MCP Pro

Coupling from real load. Module and namespace coupling drawn from real production calls. Which parts of your app actually talk, and how much.

See more →