Skip to content

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-analysisarcade-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.6872
TurboMQ 0.4268
BasicMQ 0.4268
IntraConnectivity 0.0171
InterConnectivity 0.0057
TwoWayPairRatio 0.0769

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_edges

Range 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.

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 i
epsilon_i = inter-edges touching component i (either direction, either side)
CF(i) = 2 * mu_i / (2 * mu_i + epsilon_i) # 0 if denominator is 0
TurboMQ = sum(CF(i) for all i) / num_components

The 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.

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 i
epsilon_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_components

Verified 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.

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_components

N_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.

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_pairs

Unconnected 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.

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_pairs

Range 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: AlgorithmsTools (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.