runtimeanalyz.ing ● early access · sign-up open

← All capabilities

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.

Reporting::Report reporting
progress
3,418 calls
report_row
12 calls
Enrollment enrollment
Enrollment enrollment
def report_row = Reporting::Row
  .new(student.name, progress)
Row.new ( name, String progress Float )
12 calls · cut here
Reporting::Row reporting
Reporting reads enrollments thousands of times, which is the dependency you want. One method with one caller points back the other way and closes the loop. Cut that edge and the cycle is gone.

The problem

Dependencies between parts of an app should run one way. That’s the acyclic dependencies principle: once two namespaces depend on each other, they’re one namespace in disguise. You can’t load, test, extract or reason about either without the other.

Cycles rarely arrive on purpose. Reporting has always read from Enrollment. Then someone needed a report row for an enrollment, and the obvious place to put it was on Enrollment:

module Reporting
  class Report
    def rows = enrollments.map(&:report_row)
    def progress_for(enrollment) = enrollment.progress
  end
end

class Enrollment
  def report_row = Reporting::Row.new(student.name, progress)
end

Now reporting → enrollment → reporting. Ruby won’t complain: constants resolve when the line runs, so nothing fails to load. Static tools struggle too, because a cycle can be closed by a send, a callback or a constant looked up by name, and none of those show up as a reference.

The fix

Ask for the cycles, and where to cut them:

$ ra break_cycle

cycle · reporting → enrollment → reporting
  Reporting::Report#progress_for → Enrollment#progress        3,418 calls
  Reporting::Report#rows         → Enrollment#report_row          12 calls
  Enrollment#report_row          → Reporting::Row.new             12 calls   ← cut
  report_row has 1 caller: Reporting::Report#rows
  cut Enrollment#report_row, the only edge closing the cycle       ADP

The busy edge is the one that should exist: reports read enrollments. The cycle is closed by a single method with a single caller. Move it to the side that already depends on the other:

module Reporting
  class Report
    def rows = enrollments.map { |enrollment| Row.for(enrollment) }
  end

  class Row
    def self.for(enrollment) = new(enrollment.student.name, enrollment.progress)
  end
end

Enrollment no longer mentions Reporting at all, and the dependency runs one way again.

How it works

  1. Roll calls up. Every recorded call is an edge from one method to another, and each method knows its file and namespace. Rolled up to the level you ask for, files or namespaces, that’s a directed graph with real call counts on every edge.
  2. Find the loops. Any group of namespaces that can each reach the others is a cycle. The direction of each edge is certain, because every record says which side made the call.
  3. Rank the cuts. Within a cycle, each edge is weighed by its traffic and by how many distinct call sites it takes to remove. The cheapest one to sever is flagged, with the methods that make it.

Limits

  • Runtime dependencies only. A constant that’s referenced but never called through, like a class named in a rescue clause, isn’t a recorded call, so it can’t close a cycle here.
  • Cheapest isn’t always right. The lowest-traffic edge is the least work to cut. Sometimes the right fix is to reverse a busy edge instead, and that’s a design call the counts can’t make for you.
  • Only what ran. A cycle through code no recorded run reached won’t be found.

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.

See more →

relocate_file

CLI · MCP free

Which file should move where. Two files that lean on each other constantly but sit directories apart. The graph names the folder they belong in, weighed by real call traffic and the distance between them.

See more →

check_diff

CLI · CI · MCP free

Catch it in review. Point it at a branch or PR and it flags only what the change introduced, before it lands: a drifted mock, a fresh dependency cycle, new feature envy.

See more →