Skip to content

Governance Metrics

The BDPD platform exposes a METRIC_REGISTRY of named per-turn measurements computed from arena state. Five of these — collectively the governance metrics — are written for v0.9 onwards to diagnose communication and sanctioning. This page documents the canonical formulas as implemented in engine/metrics.js; any paper or notebook citing a different formula is stale.


Overview

Metric Range Activates with Diagnostic
cooperation_index \([0, 1]\) any arena fraction of players who restrained at or below their share
lie_score \([0, \infty)\) B1 cheap-talk mean gap between announced and actual harvest, in harvest units
sanction_rate \([0, 1]\) C1 sanctioning fraction of players sanctioned this turn
announce_frequency \([0, 1]\) B1 cheap-talk fraction of players who announced
silent_defection \([0, 1]\) B1 cheap-talk fraction who skipped announcing and over-harvested

Each metric returns null when its input data is unavailable (e.g. no announcements yet, no sanctioning configured). The arena reports null rather than \(0\) so downstream aggregation distinguishes "absent" from "empty".


Per-turn state input

All five metrics consume the same per-turn state object assembled by the arena scheduler:

{
  harvests:     [number, ...],        // per-player actual harvest
  decisions:    [{ announced, actual }, ...],
  regen:        number | null,        // commons regeneration this turn
  sanctionCount: number | null,       // sanctions fired this turn
  nPlayers:     number,
}

The sustainable per-capita share is computed as \(\text{share} = \text{regen} / N\). It is the natural reference for "over-harvest": if every player harvested exactly share, the commons would be stable. Here \(N\) is the number of players who acted this turn — silent_defection and sanction_rate use nPlayers, while cooperation_index divides by the number of harvests recorded; the two coincide whenever every player harvests each turn.


Formula reference

cooperation_index

\[ \text{cooperation\_index} = \frac{\lvert \{i : \text{actual}_i \le \text{share} + \varepsilon \} \rvert}{N} \]

Fraction of players whose actual harvest did not exceed the sustainable share (with \(\varepsilon = 10^{-6}\) tolerance). \(1.0\) = full restraint; \(0.0\) = everyone over-harvested. Returns null if regen is unknown or no one played.

lie_score

\[ \text{lie\_score} = \frac{1}{|A|} \sum_{i \in A} \lvert \text{announced}_i - \text{actual}_i \rvert \]

where \(A = \{i : \text{announced}_i \ne \text{null}\}\). The mean absolute gap between announced and actual harvest, in harvest units, computed only over players who announced. Returns null when nobody announced.

Not normalised by share. Earlier drafts of the paper divided by \(\text{share}_i\); the registered implementation does not. The metric is therefore in the same units as harvest, not in a dimensionless ratio.

sanction_rate

\[ \text{sanction\_rate} = \frac{\text{sanctionCount}}{N} \]

Fraction of players sanctioned this turn. Returns null until sanctioning fires at least once. A value of \(1.0\) means every player was sanctioned this turn (rare); typical values during enforcement are \(1/N\) per violation.

Denominator is nPlayers, not violations. This choice keeps the metric on the same scale as the other governance metrics (fraction of the population, not fraction of violation events). The trade-off is discussed in the v0.9 design log.

announce_frequency

\[ \text{announce\_frequency} = \frac{\lvert \{i : \text{announced}_i \ne \text{null} \} \rvert}{N} \]

Fraction of players who emitted an intended-harvest announcement this turn. Returns null only if nPlayers is missing; \(0.0\) is reported explicitly when nobody announced.

silent_defection

\[ \text{silent\_defection} = \frac{\lvert \{i : \text{announced}_i = \text{null} \;\wedge\; \text{actual}_i > \text{share} + \varepsilon \} \rvert}{N} \]

Fraction of players who skipped announcing and also over-harvested. Captures the "real defector" archetype: extracts aggressively while staying silent. Returns null if regen is unknown or nPlayers is \(0\).

Dynamic threshold. The over-harvest test uses the live share = regen / N, not a fixed cutoff. A pre-pilot draft of the paper used a constant (\(\text{actual} > 1\)); the registered metric tracks the sustainable share as the engine state evolves.


Aggregation

Each metric registers an aggregate reducer used by governanceSnapshot() when rolling up nested-arena results. All five governance metrics aggregate by _mean (arithmetic mean over the non-null values across constituent arenas), so cross-arena reporting is well-defined whenever at least one arena produced a value.


Reading the metrics in agent code

Agents that want to react to the governance signal read the latest snapshot from the observation envelope:

function decide(obs, memory) {
  const g = obs.governance ?? {};
  const coop = g.cooperation_index ?? null;
  if (coop != null && coop < 0.5) {
    // Group cooperation is breaking down — pull back.
    return obs.myCapacity * 0.05;
  }
  return obs.myCapacity * 0.15;
}

The governance envelope is identical across nested arenas: an arena-level meta-agent reads cooperation_index the same way a world-level enforcer reads it across its child arenas.


Stability of the registry

Adding a new metric is a single-file change: register a { compute, aggregate } pair under a new key in engine/metrics.js's METRIC_REGISTRY. Engines, agents, and the governanceSnapshot() helper need not be modified — this is the engine-agnostic invariant documented in architecture.

The five governance metrics above are stable since v0.9. Future metrics will be appended; existing keys and formulas will not be changed without a versioned migration.