Metrics reference
compute_metrics returns a list of six MetricResult objects — {name, value, details} — one
per metric below. value is always a single number for the whole recovered architecture, not a
per-component report; but four of the six get there by computing something per component first
and then summing or averaging, so each section below says exactly which. Several metrics also
carry a breakdown in details (per-component cluster factors, per-component density, the raw
bidirectional-pair count) — worth reading when the headline number needs explaining, not just
displaying.
Metrics quantify the same structural signals smells flag, just continuously instead of as a threshold trip. A component can trip a smell and still barely move a whole-architecture metric — that’s expected, not a contradiction, and it’s exactly why you want both. If you haven’t read the gentle version yet, start at Smells and metrics; this page is the one you open once you already have a number and need to know what it means.
Every formula below is transcribed from the current implementation
(src/arcade_agent/algorithms/coupling.py, origin/main), not from a paper or the function’s
own docstring — where the docstring and the code disagree, that’s called out explicitly, because
the code is what actually ran.
Worked examples use arcade-agent’s own self-analysis — arcade-agent@8090d12 (v0.2.0),
9 recovered components, 331 entities, 179 dependency edges. The six values, exactly as published
in public/self/report.json:
RCI 0.6872TurboMQ 0.4268BasicMQ 0.4268IntraConnectivity 0.0171InterConnectivity 0.0057TwoWayPairRatio 0.0769RCI (Ratio of Cohesive Interactions)
Section titled “RCI (Ratio of Cohesive Interactions)”What fraction of every dependency edge in the whole architecture stays inside a single component, versus crossing a component boundary.
As implemented: a single global ratio, summed once over the whole architecture — not an average of per-component numbers. Every edge in the dependency graph is classified as intra (both endpoints in the same component) or inter (endpoints in different components); RCI is the intra share of the total.
RCI = total_intra_edges / (total_intra_edges + total_inter_edges) = total_intra_edges / total_edgesRange and direction: 0.0–1.0 (0.0 if there are no classifiable edges at all). Higher = more cohesive.
Worked example: arcade-agent scores RCI = 0.6872 — roughly 69% of all dependency edges in its own recovered architecture stay inside one component, 31% cross a component boundary.
When it misleads: RCI counts edges, not components, and it’s a sum over the whole graph, not
an average over components — so it gives no component equal footing. arcade-agent’s own
components range from 5 entities (Budget, Cache, Incremental) up to 103 (Parsers); a
single large, edge-dense component can rack up enough internal edges to carry the whole ratio,
even if several smaller components are badly wired. A “God component” that swallows most of the
codebase looks cohesive by sheer edge volume, which can mask real problems elsewhere. Contrast
with TurboMQ/BasicMQ below, which give every component equal weight regardless of size.
TurboMQ (Modularization Quality)
Section titled “TurboMQ (Modularization Quality)”An average, per-component score of how much a component’s internal wiring outweighs its connections to everything else.
As implemented: for each component i, a cluster factor CF(i) compares internal edges (counted twice) against internal-plus-external edges (also counted twice on the internal side). TurboMQ is the mean of CF(i) across components:
mu_i = intra-edges of component iepsilon_i = inter-edges touching component i (either direction, either side)
CF(i) = 2 * mu_i / (2 * mu_i + epsilon_i) # 0 if denominator is 0TurboMQ = sum(CF(i) for all i) / num_componentsThe docstring and the code disagree here, and the code wins. The function’s own docstring
says “TurboMQ = sum of cluster factors CF(i) for each component” — the classic literature
definition, which scales with the number of components and is not bounded to [0, 1]. The code
computes that sum (raw_sum, kept in details but not returned as value) and then divides it
by the component count before returning it. The number compute_metrics actually hands back is
the mean cluster factor, not the textbook sum — a bounded [0, 1] score, comparable across
architectures with different component counts, but not the same quantity the docstring names.
TurboMQ/BasicMQ as a family descend from the Modularization Quality metrics introduced by
Mitchell & Mancoridis for the Bunch clustering tool; this implementation’s normalization choice is
its own, not theirs.
Range and direction: 0.0–1.0. Higher = better modularization.
Worked example: arcade-agent scores TurboMQ = 0.4268.
When it misleads: the mean gives every component equal weight. A tiny component with one internal edge and zero external edges scores a perfect CF(i) = 1.0 and counts exactly as much as a 100-entity component with a much messier ratio — a handful of small, structurally insignificant utility components can pull the average up (or, if isolated with zero edges at all, CF(i) = 0.0 pulls it down) independent of how much of the codebase they actually represent. See also the note below: as implemented, this number and BasicMQ’s are the same computation.
BasicMQ
Section titled “BasicMQ”The same idea as TurboMQ, expressed with a differently-scaled formula in the literature — in this implementation, it’s the same computation.
As implemented:
mu_i = intra-edges of component iepsilon_i = inter-edges touching component i (identical count to TurboMQ's epsilon_i)
BasicMQ(i) = mu_i / (mu_i + 0.5 * epsilon_i)BasicMQ = sum(BasicMQ(i) for all i, where denominator > 0) / num_componentsVerified equivalence: multiplying BasicMQ(i)’s numerator and denominator by 2 gives
2*mu_i / (2*mu_i + epsilon_i) — algebraically identical to TurboMQ’s CF(i), and epsilon_i is
computed the exact same way in both functions (sum of inter-edge counts touching component i,
from the same inter dict). So under this implementation, TurboMQ and BasicMQ always compute
the same per-component term and the same mean — they aren’t two independent signals. That’s not
a guess: arcade-agent’s own self-analysis reports both at exactly 0.4268. If you ever see them
diverge on a real architecture, that’s a signal the implementation changed, not that the two
metrics are measuring genuinely different things here.
Range and direction: 0.0–1.0. Higher = better.
Worked example: arcade-agent scores BasicMQ = 0.4268 — identical to TurboMQ, for the reason above.
When it misleads: same unweighted-mean bias as TurboMQ (small components count equally with large ones). Because the two metrics are redundant here, don’t read agreement between them as two independent checks confirming each other — it’s one computation reported twice.
IntraConnectivity
Section titled “IntraConnectivity”How densely wired a component is on the inside, relative to how densely it could be wired given how many entities it has — averaged across components.
As implemented:
IntraConnectivity(i) = intra_edges(i) / (N_i * (N_i - 1)) # N_i = entity count of component i, 0 if N_i < 2
IntraConnectivity = sum(IntraConnectivity(i) for all i) / num_componentsN_i * (N_i - 1) is the maximum number of directed internal edges possible if every entity in
the component depended on every other one.
Range and direction: 0.0–1.0 in theory; in practice far below 1.0 for real code, because real dependency graphs are sparse. Higher = denser internal connections = better.
Worked example: arcade-agent scores IntraConnectivity = 0.0171. That’s expected at this
scale: Parsers, its largest component, has 103 entities, so its own denominator alone is
103 × 102 = 10,506 possible directed edges — no real component gets remotely close to fully
wired, so read this metric relative to other runs or other commits of the same repo, not against
an absolute 0–1 scale.
When it misleads: the denominator is quadratic in component size, and the average across
components is unweighted. A small component (arcade-agent has three at 5 entities each — Budget,
Cache, Incremental) needs only a couple of internal edges to post a much higher density than a
large, genuinely well-connected component ever could, and that small component’s density counts
exactly as much in the average as a 103-entity one. A couple of stray edges in a tiny component
can swing the whole-architecture number more than a real change in a big one.
InterConnectivity
Section titled “InterConnectivity”How densely two components are coupled to each other, relative to how densely they could be — averaged, but only over pairs that already have at least one edge between them.
As implemented:
InterConnectivity(i, j) = inter_edges(i, j) / (N_i * N_j) # N_i, N_j = entity counts
InterConnectivity = sum(InterConnectivity(i, j) for connected pairs) / num_connected_pairsUnconnected component pairs (zero edges between them) are not in the inter dict at all, so they
never enter the average — they aren’t scored as 0, they’re simply excluded from num_connected_pairs.
Range and direction: 0.0–1.0 in theory, far below in practice for the same sparsity reason as IntraConnectivity. Lower = less coupling between components = better.
Worked example: arcade-agent scores InterConnectivity = 0.0057 — lower than IntraConnectivity’s 0.0171, which is the shape you want: components are, on average, more densely wired inside themselves than to each other.
When it misleads: because only already-connected pairs are averaged, this metric can’t see
how many possible pairs are connected at all — it only measures how dense the connections are
among pairs that exist. A component that talks to just one other component very heavily can score
worse than a component that fans out lightly to many others, since the uncoupled pairs never drag
the second component’s average down. Read it alongside TwoWayPairRatio and the raw
component-dependency count, not alone.
TwoWayPairRatio
Section titled “TwoWayPairRatio”The fraction of component-to-component dependency relationships that go both directions.
As implemented: operates on deduplicated, direction-aware existence of a dependency
between two components (architecture.component_dependencies) — not edge counts, not weights.
Two components are “connected” if at least one entity-level edge crosses between them in either
direction; a pair is “bidirectional” if edges exist both ways.
TwoWayPairRatio = bidirectional_unordered_pairs / total_connected_unordered_pairsRange and direction: 0.0–1.0 (0.0 if there are no connected pairs). Lower = fewer mutual dependencies = cleaner, more one-directional layering.
Worked example: arcade-agent has 14 directed component-level dependency relationships across
9 components, forming 13 distinct connected pairs — and exactly one of those pairs is
bidirectional: Algorithms ↔ Tools (Algorithms → Tools and Tools → Algorithms both exist).
That gives TwoWayPairRatio = 1/13 = 0.0769, matching the published value exactly. (Derived
directly from the component_dependencies list in public/self/report.json by re-running the
pairing logic above — not asserted from the headline number alone.)
When it misleads: this is an existence check, not an intensity check. A pair with exactly one edge each direction counts identically to a pair with a heavy one-way flow plus a single stray inverse edge — the metric can’t distinguish “one back-reference worth pruning” from “genuinely mutual, load-bearing coupling.” It also only sees component-level, deduplicated relationships, so it says nothing about how many entity-level calls actually create the cycle.