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 are pinned to one self-analysis run of arcade-agent’s own repository — arcade-agent@0f3112d (v0.2.0), 10 recovered components, 359 entities, 204 dependency edges — so every derivation below stays checkable line by line. The run currently published on /self is newer (v0.3.0, arcade-agent@576e412) and its values differ; the formulas are unchanged. The six values from the pinned run:

RCI 0.6814
TurboMQ 4.5187
BasicMQ 0.4519
IntraConnectivity 0.0168
InterConnectivity 0.0059
TwoWayPairRatio 0.1333

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.6814 — roughly 68% of all dependency edges in its own recovered architecture stay inside one component, 32% 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 112 (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.

The sum, across components, of how much each 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 sum of CF(i) across components — the classic Bunch objective value:

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)

History note — this used to be a mean. Before fix(metrics): make TurboMQ the sum of cluster factors (#26), the code divided the sum by the component count while the docstring described the textbook sum; an earlier revision of this page called that divergence out. The fix made the code match the docstring: value is now the raw sum, and the mean survives as details["normalized"] (sum / k) for consumers that need a [0, 1] score. TurboMQ/BasicMQ as a family descend from the Modularization Quality metrics introduced by Mitchell & Mancoridis for the Bunch clustering tool, and the sum is their definition. Don’t compare a post-#26 TurboMQ against a value recorded before it without dividing by the component count first.

Range and direction: 0.0–k for k components — it scales with the number of components and is deliberately not bounded to [0, 1]. Higher = better modularization, but only comparable between architectures (or commits) with the same component count; use details["normalized"] or BasicMQ when the counts differ.

Worked example: arcade-agent scores TurboMQ = 4.5187 across 10 components — a mean cluster factor of 0.4519 per component (details["normalized"], and exactly what BasicMQ reports below).

When it misleads: the sum grows with the component count. Splitting one component into two — even a split that improves nothing — adds a new CF(i) term and can raise the score, so a rising TurboMQ across commits may just mean the recovery produced more components, not better ones (check num_components in details before celebrating). And every component still counts equally: a tiny component with one internal edge and zero external edges contributes a perfect CF(i) = 1.0, exactly as much as a 100-entity component with a much messier ratio. See also the note below: the per-component term is the same computation BasicMQ averages.

The same per-component score as TurboMQ, aggregated differently: BasicMQ is the mean cluster factor, where TurboMQ is the sum.

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 of the per-component term: 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). The two metrics differ only in the last step: TurboMQ sums the terms, BasicMQ divides that sum by the component count. So BasicMQ = TurboMQ / k, always — it’s the same signal on a bounded scale, not an independent check. That’s not a guess: arcade-agent’s own self-analysis reports TurboMQ = 4.5187 over 10 components and BasicMQ = 0.4519 = 4.5187 / 10, and TurboMQ’s details["normalized"] carries the identical value. If the ratio ever drifts from exactly k on a real architecture, the implementation changed.

Range and direction: 0.0–1.0. Higher = better. Unlike TurboMQ, comparable across architectures with different component counts.

Worked example: arcade-agent scores BasicMQ = 0.4519 — TurboMQ’s 4.5187 divided by its 10 components, for the reason above.

When it misleads: the unweighted mean gives every component equal weight — a handful of small, structurally insignificant utility components can pull the average up (or, if isolated with zero edges at all, a BasicMQ(i) = 0.0 pulls it down) independent of how much of the codebase they actually represent. And because BasicMQ is TurboMQ rescaled, 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.0168. That’s expected at this scale: Parsers, its largest component, has 112 entities, so its own denominator alone is 112 × 111 = 12,432 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 112-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.0059 — lower than IntraConnectivity’s 0.0168, 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 17 directed component-level dependency relationships across 10 components, forming 15 distinct connected pairs — and exactly two of those pairs are bidirectional: AlgorithmsTools and ParsersSource (edges exist in both directions for each). That gives TwoWayPairRatio = 2/15 = 0.1333, matching the published value exactly. (Derived directly from the component_dependencies list in the pinned run’s 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.