hashira

:classical_building: Package coupling metrics for Ruby, read from the AST via Prism.

Installation

gem "hashira"

Requires Ruby 3.4+.

Getting Started

hashira                  # auto-detects lib/<gem>
hashira lib/myapp        # or point it at a directory
hashira app lib          # or several — one shared graph

You get a report like:

package       TC  Ca  Ce     I  Cyc
----------------------------------------
analysis      20   1   0  0.00  -
report         7   2   0  0.00  -
ci             4   1   2  0.67  -
cli            6   0   4  1.00  -

How to Read the Numbers

Every folder under the target directory is a package. For each one:

  • TC — how many classes/modules it defines.
  • Ca — how many packages depend on it (afferent, incoming).
  • Ce — how many packages it depends upon (efferent, outgoing).
  • I — instability, Ce / (Ce + Ca), from 0 to 1.

I = 0: everyone depends on it, it depends on no one — a foundation, expensive to change. I = 1: nobody depends on it — free to change. Neither is good or bad on its own; a CLI layer should be at 1.00, a core domain layer near 0.00. The findings are about arrows pointing the wrong way:

  • SDP violation — a stable package depends on a less stable one.
  • Cycle — packages depending on each other in a loop.

Each finding comes with file-level evidence; for cycles, the shortest cycle path and its lightest edge. What a finding means for your design is your call.

How It Works

A dependency edge A→B exists when a file in package A references a constant declared by package B. Declarations are read from the AST; strings and comments are invisible. A type counts toward TC only if it defines a method directly in its body — pure namespace wrappers don't count. The root namespace is inferred (the most common outermost constant), so App::Alpha and Alpha resolve to the same package.

Each edge carries a weight: the number of constant references backing it. A root-level file x.rb folds into package x when a sibling folder x/ exists; everything else at the top level lands in (root).

CI

Fail the build when specific finding kinds exist:

hashira --fail-on cycles,sdp     # kinds: cycles, sdp

Or ratchet: commit a baseline and only allow the edge set to shrink. New edges fail with the evidence that introduced them.

hashira --ratchet                # compares against hashira_baseline.json
hashira --update-baseline        # lock in improvements
hashira --ratchet --baseline PATH

Findings accepted by design can be recorded in the baseline with a reason — they leave reports and gates, keeping a one-line reminder each:

"accepted": [
  {"kind": "sdp_violation", "package": "models", "reason": "config is generated, churn is harmless"}
]

Other Formats

hashira --json            # machine format with full evidence + edge weights
hashira --format dot      # Graphviz digraph
hashira --format mermaid  # Mermaid diagram

Why No A, D, or Zones

Classic package-metrics tools also measure abstractness (A), distance-from-main-sequence (D), and the Pain/Uselessness zones. Those assume formal interfaces are how you decouple. Idiomatic Ruby decouples via duck typing, so any abstractness proxy pins to ~0 and the "zone" verdict just restates I. Deliberately skipped.

Contributing

Bug reports and pull requests are welcome at github.com/giacope/hashira. See CONTRIBUTING.md and the code of conduct.

License

MIT